Repository navigation
perf: remove the per-field allocations from both conversion directions - #10
Merged
Merged
Conversation
Write side: - BitConverter.GetBytes allocated a byte[] for every multi byte field. Append16, Append32 and Append64 write the bytes directly in the requested order. - LINQ Reverse allocated an iterator on top of that array for every big endian field, which is why big endian cost almost twice what little endian did. - the result List<byte> grew from nothing, it is sized from the protocol now. - a string allocated a byte[] for the encoded text, a string for char.ToString() and a second byte[] for the fill character, per message. The encoder and the encoded fill bytes are resolved once while preparing, and the text is encoded straight into the backing array of the list. Read side: - big endian built a scratch byte[] per field just to feed BitConverter. BinaryPrimitives reads from a span and needs none. Ten sites. - a Guid allocated a byte[16], and for big endian a LINQ reverse plus another array. It is a stackalloc span reversed in place now. - a string copied into a temporary byte[] before decoding. It decodes straight out of the source array through a span. No new byte[ remains on either path. The fill bytes are produced with the real encoder and not as (byte) fillChar. For a character above 0x7F the ASCII encoder substitutes '?', a raw cast would emit the low byte, and that would silently change the wire format for anyone using a non ASCII fill character. No golden vector covers that case. Against the shipped 3.6.1 code: Read LE 118.09 -> 91.79 ns -22.3% 344 -> 104 B Read BE 146.21 -> 93.33 ns -36.2% 568 -> 104 B Read reused 115.58 -> 94.81 ns -18.0% 288 -> 48 B Write LE 245.33 -> 134.93 ns -45.0% 888 -> 208 B Write BE 376.86 -> 134.06 ns -64.4% 1528 -> 208 B Read string 73.96 -> 64.69 ns -12.5% 288 -> 208 B Write string 166.05 -> 144.35 ns -13.1% 384 -> 296 B Byte order is free now in both directions. Golden vectors unmoved throughout, which is the evidence that the hand written endianness arithmetic matches BitConverter byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <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.
Phase 3. Every allocation that scaled with field count is gone from both directions.
new byte[no longer appears on either path.Write side
BitConverter.GetBytesbyte[]per multi-byte field.Reverse()List<byte>growthbyte[]for the text, astringfromchar.ToString(), a secondbyte[]for the fill char — per messageThe encoder and encoded fill bytes are resolved once in
Prepare(), and text is encoded straight into the list's backing array viaCollectionsMarshal.Read side
byte[]per field, built only to feedBitConverter.BinaryPrimitivesreads from a span. Ten sites.byte[16], plus a LINQ reverse and another array for big-endian. Nowstackalloc, reversed in placebyte[]beforeGetString. Now decoded straight from the source spanThe big-endian read allocations were invisible to every test — both byte orders were correct, one was just quietly allocating 3x. Only the memory column surfaced it.
One deliberate subtlety
The fill bytes are produced with the real encoder, not
(byte) fillChar. For a character above 0x7F the ASCII encoder substitutes'?'(0x3F) while a raw cast emits the low byte — a silent wire-format change for anyone using a non-ASCII fill character. No golden vector covers that case, so it would have shipped unnoticed.Against the shipped 3.6.1 code (
bcea658)Byte order is now free in both directions (read 93.3 vs 91.8 ns; write 134.1 vs 134.9 ns).
Golden vectors unmoved throughout — that is the evidence the hand-written endianness arithmetic matches
BitConverterbyte for byte. 83/83.🤖 Generated with Claude Code