Repository navigation
Shrink ArrayPoolListWriteBuffer by renting the segment table on first spill - #6
Merged
Merged
Conversation
…nted table The ref struct held 16 byte[] references and 17 lengths inline (~240 bytes), and because GC references can never be left uninitialized, every construction zeroed all of it even when the message never left the scratch span. The chain now lives in a PooledSegment[] rented from ArrayPool<PooledSegment>.Shared on the first spill, so the struct shrinks to ~64 bytes and messages that fit the scratch span never touch it. BytesWritten is a running total instead of a loop, and ToArray/WriteTo take a direct single-copy path when nothing spilled. Measured on net10.0 (x64, 1 KB scratch): ctor+Dispose 3.6 -> 0.3 ns, one-byte write 3.3 -> 1.9 ns, one-byte ToArray 7.3 -> 4.6 ns; 200 KB unchanged, 2 KB spill 124 -> 100 ns. Steady-state allocation stays at zero on every path (verified with GC.GetAllocatedBytesForCurrentThread across Gen2 collections). CompatibleArrayPoolListWriteBuffer and BufferSegments share the same table, which removes the netstandard switch-based inline storage and the length normalization in the compatible variant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ArrayPoolListWriteBufferkept its chain of rented arrays inline: 16byte[]references and 17 lengths, about 240 bytes of struct. GC references can never be left uninitialized, so every construction zeroed the whole thing even when the message never left the scratch span, andDisposewalked it again. For tiny messages (thebyte[] Serialize<T>(T)entry point) that was most of the buffer's lifetime cost.The chain now lives in a
PooledSegment[](array + finished length pairs) rented fromArrayPool<PooledSegment>.Sharedon the first spill. The struct is ~64 bytes (two spans, three ints, a long and one reference),BytesWrittenis a running total instead of a loop, andToArray/WriteTotake a single-copy path when nothing spilled. No heap allocation is introduced: the table is pooled exactly like thebyte[]segments.CompatibleArrayPoolListWriteBufferandBufferSegmentsshare the same table, which removes the netstandard switch-based inline storage and the length normalization in the compatible variant.Measurements
net10.0, x64 (i7-13700KF), 1 KB stackalloc scratch, warmup 8 / iteration 15.
SpanWriteBuffer(floor)Steady-state allocation measured with
GC.GetAllocatedBytesForCurrentThreadacross 200 KB × 1000 (62 Gen2 GCs), 2 KB × 100k and 64 B × 100k: 0 extra bytes on every path.The remaining gap to
SpanWriteBufferis the struct staying on the stack becauseGetSpanSlowtakesref this; that is the floor of this shape.Notes
GetSpanSlow: the new segment is rented before any field changes, so a failed Rent leaves the buffer untouched.BufferSegmentsborrows the rented table. The existing contract (valid until the next write or Dispose) is unchanged, but a view used after Dispose now reads a table that may have been re-rented rather than hitting a null array.ArrayPoolListWriteBufferTestcover scratch-only, single and multiple spills, empty scratch, Dispose reset / reuse and the compatible variant. 143 tests pass.🤖 Generated with Claude Code