Following up on your note about revisiting the chunk layout to reduce per-allocation overhead. I dug into where the overhead actually comes from and what it would take to cut it, so this is a writeup to gauge the appetite before anyone writes code. No change proposed here yet.
The heap-corruption fix raised the trailing reservation from size_of::<Tag>() (1 byte) to a full word, and v5.0.4 made that explicit. Measured, that costs on average TAIL_SIZE - 1 bytes per allocation, so +7 B on 64-bit and +3 B on wasm32. It's bimodal: most allocations pay nothing, but the ones whose size mod CHUNK_UNIT lands in the top TAIL_SIZE - 1 residues (about 21.9% on 64-bit, 18.75% on wasm32) jump a whole CHUNK_UNIT (+32 B / +16 B). On a small-biased size distribution that's roughly +0.14% / +0.06% overall, so the trailing word itself is small in aggregate.
The dominant overhead for small allocations isn't that word, it's CHUNK_UNIT: a 1-byte allocation occupies a full 32 B (64-bit) / 16 B (wasm32). CHUNK_UNIT is 4 words because a free chunk needs the intrusive Node (2 words), a bin index (1 word), and its size (1 word). So cutting real overhead means shrinking that free-chunk record.
The routes I looked at, with the catches:
- 4 to 3 words (drop the stored bin, recompute from size): 24 B / 12 B isn't a power of two, so the
& !(CHUNK_UNIT - 1) mask and the align helpers break and would need real div/mod on the hot path. That trades away the CPU-neutrality the fix just kept. Not worth it on its own.
- 4 to 2 words (singly-linked free list plus dropping the bin): 16 B / 8 B stays a power of two, so the mask survives, and it would roughly halve small-alloc overhead. Two snags though: at 8-byte alignment on wasm32,
END_FLAG (bit 3) collides with a now-significant bit of the gap size and would need relocating to a bit that's still always zero; and a singly-linked list makes deregister O(k), which undoes the deallocate speedup. Doable, but a real redesign with a free-path cost.
- Safe building block: dropping the stored bin word alone (recompute via
size_to_bin, which is cheap) is a clean, behavior-preserving change. It doesn't shrink the unit by itself (still rounds to the power-of-two 4-word unit), but it frees a word for a future compact layout.
One trick that would recover the full +7 B on 64-bit native is keeping the discriminator at end - 1 there, since a gap size's most-significant byte is zero for any gap below 2^56. But that's a single-byte read again, which is exactly the endianness assumption you removed in v5.0.4, so I don't think it's worth reintroducing for ~0.14% of native memory.
So my read is that the trailing word is about optimal for the current footer-discriminator design, and the meaningful win is a 2-word unit, which means committing to an END_FLAG relocation and a different free-list scheme (or accepting O(k) frees). Worth deciding whether that's appetite-worthy before anyone starts. I can prototype the bin-word removal as a standalone step if you'd like the word freed up first.
Following up on your note about revisiting the chunk layout to reduce per-allocation overhead. I dug into where the overhead actually comes from and what it would take to cut it, so this is a writeup to gauge the appetite before anyone writes code. No change proposed here yet.
The heap-corruption fix raised the trailing reservation from
size_of::<Tag>()(1 byte) to a full word, and v5.0.4 made that explicit. Measured, that costs on averageTAIL_SIZE - 1bytes per allocation, so +7 B on 64-bit and +3 B on wasm32. It's bimodal: most allocations pay nothing, but the ones whosesize mod CHUNK_UNITlands in the topTAIL_SIZE - 1residues (about 21.9% on 64-bit, 18.75% on wasm32) jump a whole CHUNK_UNIT (+32 B / +16 B). On a small-biased size distribution that's roughly +0.14% / +0.06% overall, so the trailing word itself is small in aggregate.The dominant overhead for small allocations isn't that word, it's CHUNK_UNIT: a 1-byte allocation occupies a full 32 B (64-bit) / 16 B (wasm32). CHUNK_UNIT is 4 words because a free chunk needs the intrusive Node (2 words), a bin index (1 word), and its size (1 word). So cutting real overhead means shrinking that free-chunk record.
The routes I looked at, with the catches:
& !(CHUNK_UNIT - 1)mask and the align helpers break and would need real div/mod on the hot path. That trades away the CPU-neutrality the fix just kept. Not worth it on its own.END_FLAG(bit 3) collides with a now-significant bit of the gap size and would need relocating to a bit that's still always zero; and a singly-linked list makesderegisterO(k), which undoes the deallocate speedup. Doable, but a real redesign with a free-path cost.size_to_bin, which is cheap) is a clean, behavior-preserving change. It doesn't shrink the unit by itself (still rounds to the power-of-two 4-word unit), but it frees a word for a future compact layout.One trick that would recover the full +7 B on 64-bit native is keeping the discriminator at
end - 1there, since a gap size's most-significant byte is zero for any gap below 2^56. But that's a single-byte read again, which is exactly the endianness assumption you removed in v5.0.4, so I don't think it's worth reintroducing for ~0.14% of native memory.So my read is that the trailing word is about optimal for the current footer-discriminator design, and the meaningful win is a 2-word unit, which means committing to an
END_FLAGrelocation and a different free-list scheme (or accepting O(k) frees). Worth deciding whether that's appetite-worthy before anyone starts. I can prototype the bin-word removal as a standalone step if you'd like the word freed up first.