devinside.Marcin Borecki Ideas. Systems. Evidence.
← The notebook

Engineering

Parsing Without Unnecessary Copies: PipeReader, SequenceReader and Segment Boundaries

A real length-prefixed parser, exhaustive short-stream partitions and measured copied-versus-segmented results show why fewer allocations do not guarantee faster parsing.

Explore the mechanism ↓
Editorial ink and watercolor illustration for A message can cross every segment.

START WITH THE MECHANISM

Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.

THE FULL INVESTIGATION

Parsing Without Unnecessary Copies: PipeReader, SequenceReader and Segment Boundaries

Editorial context: 27 May 2025 · Prepared and updated: 12 November 2026
Dates and technology context

This is a retrospective editorial timeline. Topic context: 27 May 2025. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.

The concrete protocol implementation, 2,429 checks, 274 selected partitions, measurements and SDK/runtime pins are 2026-11-12 evidence, not an old experiment.

A protocol describes messages. An I/O API delivers available bytes. Much of the difficulty in writing a reliable parser comes from pretending these are the same boundary.

The first byte of a length field can arrive without the second. One read can contain several complete messages and half of the next one. A payload can span several memory segments even when the entire message has already arrived. None of these situations means the sender produced an invalid message. They mean the receiver has to preserve the distinction between the logical protocol and the storage that happens to contain it.

Copying everything into a fresh array makes that distinction easier to ignore. Sometimes the simplicity is worth its cost. Sometimes repeated copies become significant. The interesting engineering question is not whether copying is morally inferior. It is whether we can remove a particular copy while retaining a parser whose progress, lifetime and failure behavior remain understandable.

This article builds a small length-prefixed binary parser and a copied baseline. The accompanying C# project is real code: it compiled and passed 2,429 correctness checks on .NET 10.0.15, using SDK 10.0.400. That includes all 274 segment partitions of three selected short streams, exact payload comparisons, incomplete input, invalid lengths, pipe completion and cancellation. These counts describe the bounded fixture, not every possible input or a production certification.

The context and preparation date are 12 November 2026. The selected runtime is an explicitly pinned local execution environment, not a claim that these APIs are new in this release. There is no network server, customer workload or claim about an entire application's allocation rate. The performance experiment isolates parsing a complete, prebuilt batch and computing the same digest through both implementations.

Give the frame a small, explicit contract

Our wire format has two fields: a two-byte unsigned length in big-endian order, followed by that many payload bytes. A zero-length payload is valid. The largest accepted payload is 4,096 bytes. The largest legal frame therefore occupies 4,098 bytes including its header.

Payload bytes are opaque. We do not decode them into text, deserialize an object or retain a message for later processing. The consumer computes a simple order-sensitive digest and a payload-byte count, recording a marker at every frame boundary. This gives the benchmark useful work without introducing an unrelated serializer. It also means a benchmark result says nothing about the cost of a real application's message handler.

The framing parser has three possible outcomes. It returns a complete borrowed payload, reports that more input is needed, or rejects an invalid declared length. End-of-input is handled by the pipe loop: if bytes remain that cannot form a complete frame after the writer has completed, the stream is truncated and the loop fails.

This distinction prevents a common ambiguity. A one-byte header during an open connection is incomplete. The same byte at the end of a completed stream is malformed framing. Treating both as a quiet end would lose data; treating both as an immediate error would reject ordinary streaming input.

The length limit is checked as soon as the full header exists. The parser does not wait for an impossible payload to arrive before rejecting it. That decision is part of resource control: a peer cannot make this implementation deliberately retain a declared payload larger than the protocol permits. It is not a complete defense against slow input or an unlimited number of simultaneous connections. Those concerns belong to the surrounding admission and timeout policy.

For a concrete example, the bytes 00 03 41 42 43 describe a payload containing the three bytes conventionally written as ABC. The transport may present [00], [03 41], [42 43], or all five bytes together. The output contract is identical in every case. We will use this small example to reason about the parser, rather than relying on a large throughput number to make its correctness feel plausible.

A sequence is storage topology, not message structure

01 / Watch the mechanism

One frame, several storage layouts

The same five-byte frame is shown contiguous and split within its two-byte header and payload.
Enlarge diagram

Static view. Play to see how the mechanism unfolds.

The bytes 00 03 41 42 43 encode a three-byte payload. Splitting the header after 00 changes storage topology but must not change the returned payload.

Interpretation and evidence
  • A read is not a frame.
  • A segment is not a field.
  • The same bytes require the same result.
conceptual protocol illustration, matching implemented parser

ReadOnlySequence<byte> represents a logical sequence that can be backed by several memory regions. The first region is not necessarily a complete message, and a boundary between regions has no protocol significance. Treating FirstSpan as the entire input is therefore an assumption that must be justified, not a harmless shortcut.

Our test helper constructs linked ReadOnlySequenceSegment<byte> objects over slices of one fixture array. This construction deliberately controls where segment boundaries appear. It is outside the timed benchmark. The segmented parser is being asked to process an existing sequence; it is not being credited with producing the sequence for free in a real application.

Slicing creates another sequence describing a range. It does not give the application independent ownership of the underlying storage. The API exposes overloads accepting logical offsets and sequence positions, which allow the parser to select a payload without materializing that range as an array. ReadOnlySequence.Slice documentation.

This is why two separate questions matter: do all the required bytes exist in the logical input, and do they happen to occupy one physical span? Our protocol requires the first. It does not require the second. The consumer can visit each payload segment in order and update its digest without creating a contiguous payload buffer.

There are legitimate consumers that do require contiguous memory. A native API, an existing decoder or a library interface might impose that contract. In that case, copying at the consumer boundary can be the right design. The useful optimization is to avoid a copy that nobody requires, rather than to scatter complicated segmented access through components that are naturally contiguous.

The copied baseline makes exactly one full-batch array with ToArray(). It does not repeatedly copy the remaining batch for every frame. A benchmark against that deliberately wasteful alternative would mostly demonstrate a poor baseline. Here the simple implementation gets a fair advantage: after its initial copy, all parsing and payload traversal operate on contiguous spans.

Parse speculatively, then commit progress once

03 / See the idea

Commit progress only after a whole frame

The local cursor reads a header and checks the complete payload before the caller input advances.

With only 00 03 41 available, the local cursor can read length 3 but the caller input stays unchanged. Only when the final two bytes exist does the parser expose the payload and advance input.

Interpretation and evidence
  • Read through a local cursor.
  • Incomplete returns without mutation.
  • Commit once the complete frame exists.
conceptual protocol illustration, matching implemented parser

The central method is short enough to inspect as a whole. This is the implementation from lab/Protocol.cs, with its protocol constant included:

public const int MaxPayload = 4096;

public static bool TryRead(ref ReadOnlySequence<byte> input,
    out ReadOnlySequence<byte> payload)
{
    payload = default;
    var cursor = new SequenceReader<byte>(input);
    if (!cursor.TryReadBigEndian(out short signedLength)) return false;
    int length = unchecked((ushort)signedLength);
    if (length > MaxPayload)
        throw new InvalidDataException("Frame exceeds 4096 bytes.");
    if (cursor.Remaining < length) return false;
    payload = input.Slice(cursor.Position, length);
    input = input.Slice(payload.End);
    return true;
}

The important design choice is that the cursor is local. Reading the header moves this local cursor but does not yet modify the caller's input. If the payload is incomplete, the method returns false and the original sequence still begins at the frame's first header byte. The next read can present a larger sequence containing that same prefix.

Only the final assignment commits progress. At that point the method has a complete header, an acceptable length and enough payload bytes. The output payload is a borrowed view, and the caller's remaining input begins immediately after it. A valid empty payload still consumes the two-byte header, so the loop continues to make progress.

The signed short deserves attention. The available extension overload reads a signed 16-bit value. Our protocol interprets the same bits as an unsigned length, so the explicit unchecked conversion preserves the bit pattern before applying the limit. A header of FF FF must become 65,535 and be rejected. It must not become a negative length that accidentally passes a less-than-limit check.

The tagged .NET source shows that TryReadBigEndian handles the appropriate byte order and delegates a value spanning segments to a small temporary-value path. That matters to the language we use: this parser avoids allocating a full payload copy, but it does not promise that not a single byte is ever copied internally. A split two-byte header may be assembled to read the value. SequenceReader binary extensions, .NET 10 release source.

The method also avoids scanning the payload to find its end. Once the length is known, it checks logical availability and creates the slice. Our sample consumer subsequently visits the payload bytes because the digest requires them. A real decoder might perform more work, less work or additional allocations. Keep those responsibilities separate when explaining a profile.

An alternative parser could consume the header early and store the pending length in an object between reads. That is a valid state-machine design. It becomes attractive for larger headers or streaming bodies. Our small protocol favors keeping the incomplete frame intact because that makes rollback implicit and avoids another piece of durable parser state. The price is that the whole frame must become available before it is consumed.

Consumed and examined answer different questions

02 / See the idea

Consumed is not examined

Complete frames lie before consumed; an incomplete final frame lies between consumed and examined.

After two complete frames, the suffix 00 03 41 needs two more payload bytes. The reader may release the completed frames but must retain that suffix while reporting that it inspected all available input.

Interpretation and evidence
  • Consumed marks released work.
  • Examined marks inspected input.
  • Retain the incomplete prefix.
conceptual protocol illustration, matching implemented parser

The parser changes a local sequence. PipeReader needs an explicit report about what the application has finished using and how far it inspected the available buffer. These are related positions, not interchangeable names for progress.

After processing all complete frames in one ReadResult, our loop calls:

reader.AdvanceTo(remaining.Start, read.Buffer.End);

remaining.Start marks the beginning of the first incomplete frame, or the end if everything was consumed. The second position reports that the loop examined the available input while looking for another complete frame. The loop's policy is important: it drains every complete frame before requesting more input. A future fairness budget that stops after a certain number of frames would need to revisit the examined position instead of copying this line mechanically.

Consider two complete frames followed by 00 03 41. We have consumed the complete frames, but the third frame still needs two payload bytes. The consumed position stays before its 00; the examined position reaches the current buffer end. When more bytes arrive, the next buffer still contains the unconsumed header and payload prefix.

Reporting everything consumed would discard bytes needed by the next parse. Reporting no examined progress can cause the application to be offered the same incomplete data repeatedly instead of waiting for something useful to change. Neither error necessarily appears in a test that writes the entire stream before beginning the read loop.

After AdvanceTo, the previously returned buffer must no longer be accessed. The reader uses the positions to manage availability and storage lifetime. That contract also applies to payload slices pointing into that buffer. PipeReader.AdvanceTo documentation.

Our digest calculation therefore finishes synchronously inside the read iteration. It does not enqueue a borrowed ReadOnlySequence<byte> for a background worker and then advance the reader. That tempting optimization would let the producer reuse memory while another component still believes it owns a stable message. The resulting defect might look like random payload corruption rather than an obvious exception.

If downstream work must outlive the read iteration, ownership must change deliberately. Copy into independently owned storage, rent memory with an explicit return protocol, or design a different lifetime relationship that prevents advancement until consumption finishes. Holding pipe memory across a long downstream operation can be correct but expensive: it couples the producer's ability to make progress to that operation's latency.

The asynchronous loop owns completion and failure

04 / A concrete example

Four endings need different outcomes

A table separates incomplete open input, truncated completed input, oversized length and pending-read cancellation.

A missing byte is not always an error: its meaning depends on whether more input can arrive. Cancellation is an application decision to terminate, while an oversized length is rejected as soon as the header exists.

Interpretation and evidence
  • Open incomplete input waits.
  • Completed incomplete input fails.
  • Cancellation and malformed data are distinct.
conceptual protocol illustration, matching implemented parser

ReadPipeAsync is deliberately separate from the synchronous framing method. It owns the read operation, cancellation policy and reader completion. The parser itself knows nothing about sockets, tasks or the reason more input is unavailable.

The core of each read iteration is the following real code:

ReadResult read = await reader.ReadAsync(cancellationToken);
var remaining = read.Buffer;
try
{
    cancellationToken.ThrowIfCancellationRequested();
    if (read.IsCanceled)
        throw new OperationCanceledException("Pending read canceled.");

    while (TryRead(ref remaining, out var payload))
    {
        foreach (var memory in payload)
            result = result.Add(memory.Span);
        result = result.EndFrame();
    }

    if (read.IsCompleted)
    {
        if (!remaining.IsEmpty)
            throw new InvalidDataException("Truncated final frame.");
        return result;
    }
}
finally
{
    reader.AdvanceTo(remaining.Start, read.Buffer.End);
}

An outer finally calls CompleteAsync, passing the failure when an exception terminated processing. The writer belongs to the fixture and is completed by its own owner. This separation makes resource closure visible rather than relying on a method returning normally.

Completion does not mean that the last buffer is empty. A final read can contain valid frames. The loop processes them first, then checks for a truncated suffix. This ordering is necessary for small inputs and for writers that complete immediately after their last write.

Cancellation has two paths in the tests. Cancellation through the supplied token can interrupt an outstanding read. CancelPendingRead() instead causes a read result whose IsCanceled flag is true; it does not itself complete the reader. Our application deliberately converts that result into termination with OperationCanceledException. Another application could choose to resume reading, but it should make that policy explicit. PipeReader.CancelPendingRead documentation.

This implementation checks cancellation at the read boundary. It does not promise an immediate interrupt in the middle of a large synchronous digest operation. A production consumer that accepts very large batches may need to check between frames or use an explicit work budget. That would also require careful examined-position handling if complete frames remain. The sample is intentionally small, and its cancellation tests establish the waiting-read behavior rather than a universal response-time bound.

Invalid input is terminal. The parser does not attempt to scan for a new plausible length after an oversized header. With no magic value, escaping or independent integrity marker, such resynchronization would be guesswork. A different protocol can support recovery, but that recovery must be designed into its grammar.

A frame limit is not a process-memory limit

The 4,096-byte maximum constrains one payload. It does not bound the number of concurrent connections, the size of a producer write, retained segment capacity or a downstream queue. Calling the application memory-bounded because it has a frame-length check would skip most of the system.

The real-pipe fixture uses a pause threshold of 8,192 bytes and a resume threshold of 4,096. These are fixture settings, not a measured optimum. The pause threshold tells the writer when flushing should wait for reader progress; it should not be described as a hard cap on the process's memory. A writer can obtain and fill memory before it flushes. PipeOptions.PauseWriterThreshold documentation.

The relationship between framing and backpressure is especially important here. Our parser does not consume an incomplete frame. If a producer flushes part of a valid frame and becomes blocked at a threshold too small to accommodate the rest, while the reader waits for the rest before consuming anything, neither participant has a path to useful progress. For this protocol and producer pattern, the threshold must allow the complete legal frame to arrive, or the design must change how partial frames are stored and consumed.

There is also a distinction between logical bytes retained and backing storage retained. A short unconsumed prefix can keep a larger segment's memory alive. Our tests establish that the correct prefix remains visible. They do not measure pooled capacity, retained heap or the memory behavior of a network server.

To investigate those separately, record outstanding logical bytes, allocation activity and retained memory after comparable phases. Include a slow consumer and many simultaneously active streams only in a separately bounded experiment. The present benchmark deliberately avoids that load: it answers a local parsing question and must not create artificial pressure on the shared machine to support a larger claim.

Microsoft's pipelines guidance is useful precisely because it treats framing and buffering as a coordinated problem rather than suggesting that one read corresponds to one message. The sample here is an original, deliberately narrow implementation of that separation. System.IO.Pipelines overview.

Exhaust segment boundaries before measuring speed

For a stream of length n, each of the n minus one gaps between bytes can either contain a segment boundary or not. Enumerating every combination therefore produces every partition into nonempty segments. The short fixtures use lengths two, five and nine, giving 2, 16 and 256 partitions respectively: 274 in total.

The two-byte stream is an empty frame. The five-byte stream has a three-byte payload. The nine-byte stream contains three frames, including an empty middle frame. This last case matters because correctness includes progress across adjacent headers and zero-length payloads, not just reconstructing one attractive example.

For each partition, both implementations compute a digest. More importantly, the test calls TryRead repeatedly and compares each returned payload byte-for-byte with the original payload arrays used before framing. Digest agreement alone would be inadequate: the two implementations share the digest routine and a hash can collide. Exact payload comparison makes ordering and frame membership independently visible.

Additional tests insert empty memory segments, present every incomplete prefix of a selected frame and verify that incomplete parsing leaves the input position and length unchanged. Headers declaring 4,097 and 65,535 bytes are rejected. A payload at the 4,096-byte limit is accepted across small segments. These boundaries test the contract rather than merely reproducing the shape of the happy-path loop.

The pipe tests exercise a different dimension. They write a stream using chunk sizes from one byte through the complete selected stream, allowing the consumer to run between writes. These are read-arrival schedules; the pipe implementation remains free to choose its backing segments. Direct sequence tests force storage topology, while pipe tests check integration with asynchronous arrival. One category does not replace the other.

We also test an empty completed stream, completion after one header byte, completion during a payload, an invalid header through the real pipe, token cancellation while waiting and CancelPendingRead. The successful local run reported 2,429 checks and no failed check. The runner exits with a failure if an expected exception is absent or an assertion does not hold.

There are clear limits. This is not exhaustive over every possible payload value, length or interleaving of concurrent producers. There is only one reader and one writer owner. It does not test malicious network peers, TLS framing or application handlers with external effects. If those responsibilities are added, their acceptance criteria must be added too, rather than inherited from the existing green count.

Compare the simple copy against the segmented path fairly

05 / See the idea

Fewer allocations did not always mean faster

Measured segmented-to-copied median time ratios for all nine cases range from 0.63 to 2.47; all small payload cases are slower.

The local run measured zero managed allocation in the segmented timed loop, yet all 16-byte payload cases took more than twice as long as the copied baseline. Ratios below one favor the segmented path; these are complete-batch parser results, not network latency.

Interpretation and evidence
  • Keep every configured case.
  • A ratio above one means slower.
  • Allocation and elapsed time are separate results.
measured; benchmark-results.json; 2026-11-12

The copied implementation begins by materializing the complete input batch:

byte[] contiguous = input.ToArray();
ReadOnlySpan<byte> remaining = contiguous;
Digest result = Digest.Empty;
while (!remaining.IsEmpty)
{
    if (remaining.Length < 2)
        throw new InvalidDataException("Truncated header.");
    int length = BinaryPrimitives.ReadUInt16BigEndian(remaining);
    if (length > MaxPayload)
        throw new InvalidDataException("Frame exceeds 4096 bytes.");
    if (remaining.Length - 2 < length)
        throw new InvalidDataException("Truncated payload.");
    result = result.Add(remaining.Slice(2, length)).EndFrame();
    remaining = remaining.Slice(2 + length);
}

Both paths see the same bytes, validate the same maximum length, preserve the same frame boundaries and compute the same digest. The copied version pays for a contiguous batch and then benefits from straightforward span traversal. The segmented version avoids that batch allocation but repeatedly navigates sequence structure. It can perform more bookkeeping, especially when small segments fragment large payloads.

The benchmark constructs 64 frames per batch. It uses payload lengths of 16, 256 and 4,096 bytes. For each size it compares a contiguous sequence with fixed segment sizes of 17 and 257 bytes. These are designed cases: the segment sizes do not align neatly with every frame, so they create crossings without requiring a network service.

All arrays and segment nodes are built before timing. Both variants receive warm-up calls. Seven samples alternate which implementation runs first, with 200 batch parses per sample. Each timed loop preserves its result through a sink. Full collections happen before each sample, outside the stopwatch, so the experiment starts from a more controlled point but does not reproduce normal long-running application GC behavior.

The measurement boundary includes framing and payload digest work. It excludes input production, pipe scheduling, network I/O, object deserialization, persistence and result publication. The allocation counter covers the current synchronous thread. It does not report native allocation or the amount of memory still alive after collection. GC.GetAllocatedBytesForCurrentThread documentation.

The explicit copied-byte count is derived from the implementation: the copied path materializes the whole wire batch, while the segmented path does not explicitly materialize a payload or batch. That count is not a hardware memory-bandwidth measurement. It excludes the runtime's tiny header handling and other internal data movement.

The performance run belongs to a separate exclusive measurement lane. Concurrent article generation or unrelated builds should not be mistaken for part of a controlled timing environment. The raw report records runtime, operating system, architecture, processor count, stopwatch frequency, GC mode and every sample. A summarized result should retain the raw observations and use the actual run date.

The exclusive local run completed on 12 November 2026 at 13:41 UTC, with exit code zero. It used .NET 10.0.15, X64, workstation GC, and Windows build 20348 on a shared development host exposing 64 logical processors. Other task benchmarks did not run simultaneously. That is useful coordination, not a claim that the machine was an isolated performance laboratory.

The following table includes every configured case. Times are the median of seven samples, in microseconds per 64-frame batch. The ratio divides segmented time by copied time, so values greater than one mean the segmented implementation was slower. Allocation is managed bytes per batch for copied / segmented, measured on the current thread.

Payload bytes per frame Segment bytes Copied µs Segmented µs Time ratio Allocated bytes, C / S
16 contiguous 8.94 19.23 2.15 1,176 / 0
16 17 11.27 27.86 2.47 1,176 / 0
16 257 8.97 22.06 2.46 1,176 / 0
256 contiguous 22.61 21.44 0.95 16,536 / 0
256 17 27.06 23.73 0.88 16,536 / 0
256 257 20.94 22.33 1.07 16,536 / 0
4,096 contiguous 414.63 316.46 0.76 262,296 / 0
4,096 17 542.50 343.54 0.63 262,296 / 0
4,096 257 420.25 319.47 0.76 262,296 / 0

The small-frame result is the first thing to notice: the zero-allocation segmented path was slower in all three 16-byte cases. Its median time was between 2.15 and 2.47 times that of the copied baseline. The experiment therefore rejects a blanket claim that removing the array must improve elapsed time. Sequence traversal and framing bookkeeping are plausible contributors, but this run does not isolate those mechanisms with a profiler or disassembly.

At 256-byte payloads the answer was mixed. The segmented median was lower for contiguous input and 17-byte segments, but higher for 257-byte segments. In the contiguous case, one segmented sample reached 93.23 µs while the median was 21.44 µs; copied samples ranged from 20.46 to 26.37 µs. Hiding that spread behind a neat winner label would overstate the stability of the observation. The raw samples remain in benchmark-results.json.

For the 4,096-byte payload, segmented medians were lower in all three configurations. The ratios were 0.76, 0.63 and 0.76. This is a result for this complete-batch digest workload, not a promised improvement for a socket server. No confidence interval or population-level significance claim is attached to seven within-process samples.

Allocation followed the implemented boundary consistently: the copied variant allocated 1,176, 16,536 or 262,296 bytes per batch, while the segmented variant recorded zero in each case. Those amounts apply to the timed parser-and-digest loop. The already allocated input arrays and segment objects, the benchmark's report objects, native memory, pipe buffers and retained heap are outside that number. The largest copied array also belongs to a different allocation-size regime than the smallest; this design does not independently isolate the resulting GC effects.

The honest finding is a trade: this implementation eliminated the measured per-batch allocation while losing elapsed-time performance for small frames and some medium-frame layouts. Larger payloads benefited in the observed run. That is enough evidence to reject a universal slogan, and enough to motivate testing the distribution that an actual application receives. It is not enough to announce an end-to-end latency improvement.

Read a result without promoting it into a bigger promise

A lower allocation count establishes something useful but narrow: fewer managed bytes were allocated within the measured synchronous boundary. It does not establish lower latency in an endpoint whose dominant cost is a database call. It does not prove lower retained memory when borrowed pipe segments remain alive behind a slow consumer.

Likewise, a faster copied baseline would not make segmented parsing a mistake. It could show that contiguous access and simpler bookkeeping dominate for a particular small batch or highly fragmented layout. If allocation pressure is harmless in the actual application and the copied representation makes downstream processing safer, the simpler design may be the right result.

The reverse caution also applies. If the segmented implementation is faster and allocates less in this fixture, that does not justify replacing every string parser. Text decoding, Unicode semantics, escaping and error recovery are absent from this protocol. A line-oriented administrative format with tiny input volumes may benefit more from clear string processing than from a complex lifetime protocol.

Before applying the result, locate the corresponding copy in the real profile. Ask who requires contiguous storage, how long the message must live and whether the same bytes are copied again elsewhere. Removing one array only to reconstruct it in the next layer may improve a local chart while leaving the application unchanged.

The most valuable conclusion can therefore be a boundary choice. Keep framing and immediate validation over borrowed segments. Materialize an owned message only when asynchronous downstream work requires it. Alternatively, keep a single copy if that is the natural ownership handoff and its measured cost is acceptable. The goal is a defensible placement of work, not the disappearance of every allocation from a code listing.

Reproduce the fixture and know when to stop

The project has no external package restore dependency beyond the declared framework reference. Its local global.json selects SDK 10.0.400, and the project pins runtime 10.0.15 with roll-forward disabled. This makes an unavailable environment fail visibly instead of silently running a different runtime. Install or change pins deliberately when reproducing elsewhere, and record the actual environment rather than editing the report to match the original.

From the lab directory, the correctness command is:

dotnet run -c Release -- test

The isolated measurement command, after a successful build, is:

dotnet run -c Release --no-build -- bench ../benchmark-results.json

The test command is the first gate. Do not use a faster result from a parser that drops an incomplete prefix or changes the interpretation of an unsigned length. Do not convert expected malformed-input exceptions into a silent success simply to make a throughput run finish.

After correctness, decide which performance question remains. The included benchmark answers the cost of parsing and digesting prebuilt complete batches under specified segmentation. A question about retained pipe memory, scheduling fairness or slow clients needs another bounded fixture and another report. It should not be smuggled into the conclusion of this one.

The parser's central invariant is simple: bytes become consumed only after the complete permitted frame exists, and borrowed bytes stop being used before the reader advances. Segment boundaries can then vary without changing message meaning. Once those rules are explicit, performance work becomes easier to reason about because every proposed optimization has something concrete it must preserve.

That is the practical value of avoiding unnecessary copies. It is not a claim that zero-copy automatically means good architecture. It is a way to keep the protocol, storage topology and ownership boundary separate enough that we can measure one decision without losing control of the others.

Source code and execution evidence

Download the complete lab, source record and raw execution results. This package supports the specific execution claims in this article. It is not a production certification.

THE AUTHOR

Marcin Borecki

Developer, entrepreneur and business angel. Writing about AI, software, data and the evidence behind technical decisions.

About the author →