Repository navigation
Write File span and memory payloads without copying them - #701
Merged
Merged
Conversation
This was referenced Oct 12, 2026
This was referenced Oct 14, 2026
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.
Three of the
Filespan/memory polyfills (active below net9) copy the whole payload withToArray()before writing it:AppendAllBytes(string, ReadOnlySpan<byte>)WriteAllBytesAsync(string, ReadOnlyMemory<byte>, CancellationToken)AppendAllBytesAsync(string, ReadOnlyMemory<byte>, CancellationToken)Change
Stream.Write(ReadOnlySpan<byte>)/Stream.WriteAsync(ReadOnlyMemory<byte>).MemoryMarshal.TryGetArrayreaches the backing array and writessegment.Array, segment.Offset, segment.Count; only memory that is not array backed is still copied.ArrayPoolbuffer of at most 81,920 bytes, a chunk at a time, rather than into a new array the size of the payload.AppendAllBytesAsync(ReadOnlyMemory)used to forward to thebyte[]overload; it now opens its own stream so that it can write the memory. That stream is opened the wayWriteAllBytesAsyncnext to it opens its stream (bufferSize: 4096, useAsync: true).WriteAllBytes(string, ReadOnlySpan<byte>)is left alone; its copy is documented by its//Note:.Measurements
net48, x64, Release, writing to a temp file. Time includes opening and closing the file and is noisy from run to run (file system); allocated bytes are per call on the calling thread.
AppendAllBytes(span)WriteAllBytesAsync(memory)AppendAllBytesAsync(memory)AppendAllBytes(span)WriteAllBytesAsync(memory)AppendAllBytesAsync(memory)AppendAllBytes(span)WriteAllBytesAsync(memory)AppendAllBytesAsync(memory)At 1 KB the time differences are noise; the saving is the payload-sized array. In the 1 KB
AppendAllBytesAsync(memory)row the "before" figure does not include theFileStream's 4 KB buffer, because the old code wrote through a synchronous stream on a pool thread, which the per-thread counter does not see.Tests
In a unique temp directory that is deleted afterwards, for payloads of 0, 1, 100, 4096, 5000, 81,920, 81,921 and 300,000 bytes (empty, below and above the
FileStreambuffer, at and past the chunk size):AppendAllBytes(span): creates the file, then appends a slice taken from the middle of a larger array; only the slice is written and the source array is untouched. Also astackallocspan.WriteAllBytesAsync(memory): a slice of an array (non-zero offset) and a slice of aMemoryManager<byte>thatTryGetArraycannot see through; over an existing longer file (truncated) and to a missing file.AppendAllBytesAsync(memory): the same two sources, creating the file and then appending twice.default(ReadOnlyMemory<byte>)for both memory overloads, and a missing directory for all three.The existing test that an already cancelled token neither creates nor truncates the file covers both memory overloads.