Skip to content

Read directly from a ReadOnlySpan, no copy into a byte array - #11

Merged
holonsoftFounder merged 1 commit into
mainfrom
feat/v4-span-read
Sep 13, 2026
Merged

holonsoftFounder merged 1 commit into
mainfrom
feat/v4-span-read

Conversation

@holonsoftFounder

Copy link
Copy Markdown
Member

What

ConvertFromByteArray gets ReadOnlySpan<byte> overloads in both shapes (new instance and reused instance). The span version is now the real implementation; the byte[] overloads keep their null check and delegate to it.

Why

The converter could only be handed a byte[]. A caller whose bytes arrive in anything else, a socket receive buffer, a rented buffer, a slice of a larger frame, had to copy them into a fresh array first. That copy is bigger than everything the converter allocates per message, so it dominated what Phase 3 saved.

Numbers

Reading a 38 byte frame that sits at a non zero offset inside a larger receive buffer:

time alloc
copy into an array first 132.05 ns 168 B
span, no copy 75.13 ns 104 B
span, into a reused instance 77.98 ns 48 B

Honest attribution: roughly 50 ns of that gap is the FluentConditions guard clause the array overload runs and the span overload does not, measured separately in the same run. The copy itself accounts for the rest plus the 64 bytes. Removing that guard from the per message path is the next PR and lifts both overloads.

Safety

  • 96/96 tests green, 13 of them new in TestSpanApi.cs.
  • The golden vectors did not move, so the wire format is unchanged.
  • New tests cover: span vs array equality for fixed position, sequence, advanced types (Guid/DateTime/padding), decimal/sbyte and big endian protocols; a frame read out of the middle of a larger buffer; a stackalloc frame; reused instances; truncated and empty spans rejected as ProtocolConverterException; null arrays still rejected; Prepare() still required.
  • The equality tests compare every field by reflection, so a field added to a test POCO later is covered automatically.

Breaking

IProtocolConverter<T> gains two members. That breaks hand written implementations and mocks, which is acceptable for the 4.0 major.

🤖 Generated with Claude Code

…rray

The converter could only be handed a byte[]. A caller whose bytes arrive in
anything else, a socket receive buffer, a rented buffer, a slice of a larger
frame, had to copy them into a fresh array first, and that copy is larger than
everything the converter itself allocates per message.

ConvertFromByteArray now has ReadOnlySpan<byte> overloads in both shapes, new
instance and reused instance. The span version is the real implementation, the
array versions keep their null check and delegate to it, so both directions
stay byte identical, which the golden vectors confirm.

Reading a 38 byte frame out of a larger receive buffer:

  copy into an array first   132.05 ns   168 B
  span, no copy               75.13 ns   104 B
  span, reused instance       77.98 ns    48 B

Roughly 50 ns of that gap is the FluentConditions guard clause that the array
overload runs and the span overload does not, measured separately, so the copy
itself accounts for the remaining part plus the 64 bytes. Removing that guard
from the per message path is the next step and will lift both overloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@holonsoftFounder
holonsoftFounder merged commit 0cc8525 into main Sep 13, 2026
2 checks passed
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