Skip to content

Put a hash value's length in the blob instead of beside the offset - #177

Merged
tamnd merged 1 commit into
mainfrom
hash-value-ref
Sep 1, 2026
Merged

tamnd merged 1 commit into
mainfrom
hash-value-ref

Conversation

@tamnd

@tamnd tamnd commented Sep 1, 2026

Copy link
Copy Markdown
Owner

A hash field's row payload was a Span, which is an offset and a length at four bytes each. The length is the part that did not need to be there. A value already has somewhere to keep its own length, which is the blob it sits in, and writing it in front of the bytes costs one byte for anything under two hundred and fifty five bytes and five for anything longer.

So the payload is now the offset on its own and a field is three bytes cheaper. It is the same trick the element table already plays with a member name, where the length lives in the eight spare bits of the row, and it lands in the same place. blob.rs now describes both reference shapes and neither of them is a compromise with the other, because the two callers have different amounts of room to work with. A member name is capped at two hundred and fifty five bytes by the row it lives in, and a hash value cannot be capped at anything under the half gigabyte Redis allows.

The one thing that got slower is HSTRLEN, which used to read the length straight out of the row and now takes a byte off the front of the value. That byte is on the same cache line as the value, so it costs the miss a caller would have taken to read the value anyway.

What a field costs now

On an eight byte field name and an eight byte value: eight bytes of row, four of offset, one of value length, about five of slot array, and the two things the caller actually stored. Eighteen bytes of overhead where it used to be twenty one.

Twelve of those eighteen are the row and the slot array. The only way past them is to stop interning field names, which would put the name back in front of every value and pay for it again on every rewrite, so eighteen is the floor for this layout. M3's memory row wants sixteen, and that gap is the slot array, which is the same argument as #175 and belongs there rather than here.

Measured

memory_bytes() reports capacity rather than live bytes, so the in process number moves in steps and in this case it moved in both directions at different sizes, because the extra byte in front of each value crossed a growth threshold on the value blob. Peak RSS on Linux is the honest reading and it is what the gate scores, so here it is from server3, five hundred thousand fields of field-%08d and value-%08d.

structure per field peak RSS
main 28,382,972 56.77 27,576 KiB
this branch 27,939,793 55.88 26,512 KiB

Peak comes down by 1,064 KiB, which is 2.18 bytes a field against the three the arithmetic says, and the difference between those two is the value blob's own growth rounding. The structure column is the steppy one and it agrees on the direction.

Part of M3 (#4).

A hash field's row payload was a Span, which is an offset and a length at four bytes each. The length is the part that did not need to be there. A value already has somewhere to keep its own length, which is the blob it sits in, and writing it in front of the bytes costs one byte for anything under two hundred and fifty five bytes and five for anything longer.

So the payload is now the offset on its own and a field is three bytes cheaper. It is the same trick the element table already plays with a member name, where the length lives in the eight spare bits of the row, and it lands in the same place: blob.rs now describes both, and neither of them is a compromise with the other because the two callers have different amounts of room to work with.

The one thing that got slower is HSTRLEN, which used to read the length straight out of the row and now takes a byte off the front of the value. That byte is on the same cache line as the value, so it costs the miss a caller would have taken to read the value anyway.

Per field, before and after, on an eight byte field name and an eight byte value: eight bytes of row, four of offset, one of value length, about five of slot array, and the two things the caller actually stored. Eighteen bytes of overhead where it used to be twenty one. Twelve of the eighteen are the row and the slot array, and the only way past those is to stop interning field names, which would put the name back in front of every value and pay for it again on every rewrite.
@tamnd
tamnd merged commit 1da027f into main Sep 1, 2026
20 checks passed
@tamnd
tamnd deleted the hash-value-ref branch September 1, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant