Skip to content

Replace heap-allocated temporary byte arrays with ArrayPool<byte>.Shared rentals in CipherStream and Streams to reduce GC pressure on hot I/O paths#664

Open
paulomorgado wants to merge 1 commit intobcgit:masterfrom
paulomorgado:performance/array-pool
Open

Replace heap-allocated temporary byte arrays with ArrayPool<byte>.Shared rentals in CipherStream and Streams to reduce GC pressure on hot I/O paths#664
paulomorgado wants to merge 1 commit intobcgit:masterfrom
paulomorgado:performance/array-pool

Conversation

@paulomorgado
Copy link

Describe your changes

Replace heap-allocated temporary byte arrays with ArrayPool<byte>.Shared rentals in CipherStream and Streams to reduce GC pressure on hot I/O paths.

Both CipherStream and Streams are used heavily in cryptographic I/O pipelines where temporary buffers are allocated on every read/write call. These short-lived allocations put unnecessary pressure on the garbage collector. ArrayPool<byte>.Shared avoids this by reusing buffers from a shared pool.

CipherStream.cs

Method Before After
Write(byte[], int, int) new byte[outputSize] ArrayPool.Rent / Return under NETCOREAPP2_0_OR_GREATER
WriteAsync(byte[], int, int) new byte[outputSize] ArrayPool.Rent with async ownership transfer via WriteAsyncArrayPoolCompletion
Write(ReadOnlySpan<byte>) stackalloc / new byte[] stackalloc / ArrayPool.Rent
WriteAsync(ReadOnlyMemory<byte>) new byte[outputSize] ArrayPool.Rent with async ownership transfer via WriteAsyncCompletion
Dispose(bool) stackalloc / new byte[] stackalloc / ArrayPool.Rent

Streams.cs

Method Before After
CopyTo stackalloc / new byte[] stackalloc / ArrayPool.Rent
CopyToAsync new byte[bufferSize] ArrayPool.Rent / Return
ReadAsyncCompletion new byte[buffer.Length] ArrayPool.Rent, deferred into async method body
WriteAsyncCompletion buffer.ToArray() ArrayPool.Rent, deferred into async method body

Design notes

  • Ownership transfer in async methods: When a rented buffer must outlive the synchronous portion of a method (because it's passed to an async write), ownership is transferred to a completion helper that returns the buffer in its finally block. The synchronous finally only returns the buffer if no async handoff occurred (checked via length == 0).
  • stackalloc / ArrayPool pattern: For synchronous Span-based methods, small buffers still use stackalloc for zero-allocation fast paths. Only the large-buffer fallback uses ArrayPool (previously new byte[]).
  • clearArray: true: All ArrayPool.Return calls use clearArray: true to zero sensitive cryptographic data before the buffer is returned to the pool, matching the previous Array.Clear / Span.Fill(0x00) behavior.
  • Conditional compilation: New ArrayPool usage in methods shared across all TFMs is gated behind #if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER, falling back to the original new byte[] allocation for older targets.

How has this been tested?

Build verified across all target frameworks (net461, netstandard2.0, net6.0) with 0 errors.

Checklist before requesting a review

  • I have performed a self-review of my code
  • I have kept the patch limited to only change the parts related to the patch
  • This change requires a documentation update

See also Contributing Guidelines.

 Use ArrayPool<byte> for temporary buffers in CipherStream and Streams

 Replace heap-allocated temporary byte arrays with ArrayPool<byte>.Shared
 rentals to reduce GC pressure on hot paths. All rented buffers are
 returned with clearArray: true to maintain existing security-clearing
 behavior.

 CipherStream:
 - Write(byte[], int, int): ArrayPool under NETCOREAPP2_0_OR_GREATER
 - WriteAsync(byte[], int, int): ArrayPool with async ownership transfer
 - Write(ReadOnlySpan<byte>): stackalloc/ArrayPool instead of stackalloc/new
 - WriteAsync(ReadOnlyMemory<byte>): ArrayPool with async ownership transfer
 - Dispose: stackalloc/ArrayPool instead of stackalloc/new

 Streams:
 - CopyTo: stackalloc/ArrayPool instead of stackalloc/new
 - CopyToAsync: ArrayPool instead of new byte[]
 - ReadAsyncCompletion: ArrayPool, deferred into async method body
 - WriteAsyncCompletion: ArrayPool, deferred into async method body
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