ArrayPoolBufferWriter<T> Class
A high-performance implementation of IBufferWriter<T> that uses pooled memory segments from ArrayPool<T>.
Namespace
CryptoHives.Foundation.Memory.Buffers
Inheritance
Object → ArrayPoolBufferWriter<T>
Implements
IBufferWriter<T>IDisposableIResettable
Syntax
public sealed class ArrayPoolBufferWriter<T> : IBufferWriter<T>, IDisposable, IResettable
Type Parameters
T - The type of elements in the buffer
Overview
ArrayPoolBufferWriter<T> provides an efficient way to build sequences of data using pooled memory segments. It implements IBufferWriter<T>, making it compatible with serializers and other APIs that write to buffers. The writer grows by allocating progressively larger chunks from the array pool, avoiding continuous reallocations.
Benefits
- Pooled Memory: Uses
ArrayPool<T>.Sharedto minimize allocations - ArrayPool Backed: Efficient recycling of arrays for high-performance scenarios
- Buffer Clear Option: Optionally clears arrays before returning to pool for privacy
- Progressive Growth: Chunks grow exponentially up to a maximum size
- Zero-Copy Access:
GetReadOnlySequence()provides direct access without copying - Escapes the Scope:
LeaseSequence()hands the payload on with its own lifetime, at no cost - IBufferWriter Support: Works with
System.Text.Json, Protocol Buffers, and other modern serializers - Disposable: Returns arrays to the pool on disposal
- Poolable: The writer object itself can be recycled, not just its buffers
- Configurable: Customizable chunk sizes and clearing behavior
Constructors
| Constructor | Description |
|---|---|
ArrayPoolBufferWriter() |
Creates with default settings (256-element initial chunks, 64K max) |
ArrayPoolBufferWriter(int defaultChunkBytes, int maxChunkBytes) |
Creates with custom chunk sizes |
ArrayPoolBufferWriter(bool clearArray, int defaultChunkBytes, int maxChunkBytes) |
Creates with full customization including array clearing |
All three throw ArgumentOutOfRangeException when defaultChunkBytes is less than one.
To rent a pooled writer instead of constructing one, see
ArrayPoolBufferWriterProvider<T> and
ObjectPools.RentBufferWriter<T>().
Constants
| Constant | Value | Description |
|---|---|---|
DefaultChunkBytes |
256 | Size, in bytes, of the first chunk rented |
MaxChunkBytes |
65,536 | Ceiling, in bytes, that a chunk may grow to |
The budgets are bytes, not elements
A chunk budget expressed in elements means something different for every T: 65,536 elements is
64 KiB of byte but 512 KiB of long. An object reaches the large object heap at 85,000 bytes,
so an element-count ceiling silently puts every wide element type there.
Budgeting in bytes makes one number mean the same thing everywhere — the writer divides by the size of one element to decide how many fit:
T |
bytes/element | elements per chunk | chunk size |
|---|---|---|---|
byte |
1 | 65,536 | 64 KiB |
short |
2 | 32,768 | 64 KiB |
int |
4 | 16,384 | 64 KiB |
long, any reference type |
8 | 8,192 | 64 KiB |
Guid |
16 | 4,096 | 64 KiB |
64 KiB is not an arbitrary ceiling. ArrayPool<T> serves a request from a bucket of 16 × 2ⁿ
elements, rounding up, and the bucket above 65,536 bytes is 131,072 — there is nothing in between.
So 64 KiB is the largest budget whose rented array is guaranteed to stay off the LOH.
For the same reason the element count is rounded down to a power of two, so it matches its bucket exactly. Without that, a 12-byte element would divide 64 KiB into 5,461, which ArrayPool would serve from the 8,192 bucket — 98,304 bytes, back over the threshold. Rounding down also wastes nothing: the span handed out fills its bucket, where an unrounded count leaves the tail of the array unused.
For byte the two units coincide, so nothing about that case changed.
Methods
IBufferWriter Implementation
public void Advance(int count)
Advances the writer by the specified number of elements that were written to the span/memory obtained from GetSpan/GetMemory.
public Memory<T> GetMemory(int sizeHint = 0)
Returns a Memory<T> to write to. The memory is at least sizeHint elements large.
public Span<T> GetSpan(int sizeHint = 0)
Returns a Span<T> to write to. The span is at least sizeHint elements large.
Sequence Access
public ReadOnlySequence<T> GetReadOnlySequence()
Returns a ReadOnlySequence<T> representing all written data. The sequence borrows the writer's buffers: it is valid only until the next write operation or disposal.
public SequenceLease<T> LeaseSequence()
Pairs the written data with this writer as a single disposable value, so the payload can leave the scope that produced it. Disposing the SequenceLease<T> disposes the writer, which returns it to its pool if it was rented, and its buffers to the array pool.
This allocates nothing — the lease is a struct and the sequence is the one the writer already holds. It is the usual way to hand a payload to a caller.
static SequenceLease<byte> BuildPayload()
{
var writer = ObjectPools.RentBufferWriter<byte>(); // deliberately not `using`
Serialize(writer);
return writer.LeaseSequence();
}
using SequenceLease<byte> payload = BuildPayload();
Send(payload.Sequence);
Reset and Disposal
public bool TryReset()
Restores the writer to its just-constructed state, returning every rented array to ArrayPool<T>.Shared. Returns false if the writer has been disposed and must be discarded. Implements IResettable from Microsoft.Extensions.ObjectPool, so a DefaultObjectPool<T> recycles instances without a custom policy. Idempotent.
public void Dispose()
For a writer rented from a pool, resets the instance and returns it to that pool. Otherwise returns all pooled arrays to ArrayPool<T>.Shared and invalidates the writer.
Usage Examples
Basic Usage
using var writer = new ArrayPoolBufferWriter<byte>();
// Get span and write
Span<byte> span = writer.GetSpan(100);
for (int i = 0; i < 100; i++)
{
span[i] = (byte)i;
}
writer.Advance(100);
// Get the result
ReadOnlySequence<byte> sequence = writer.GetReadOnlySequence();
With JSON Serialization
using var writer = new ArrayPoolBufferWriter<byte>();
using var jsonWriter = new Utf8JsonWriter(writer);
jsonWriter.WriteStartObject();
jsonWriter.WriteString("name"u8, "value"u8);
jsonWriter.WriteEndObject();
await jsonWriter.FlushAsync();
ReadOnlySequence<byte> jsonBytes = writer.GetReadOnlySequence();
mqttClient.Publish("topic", jsonBytes);
Building Protocol Messages
using var writer = new ArrayPoolBufferWriter<byte>();
// Write header
Span<byte> header = writer.GetSpan(4);
BinaryPrimitives.WriteInt32LittleEndian(header, messageId);
writer.Advance(4);
// Write payload
payload.CopyTo(writer.GetSpan(payload.Length));
writer.Advance(payload.Length);
ReadOnlySequence<byte> message = writer.GetReadOnlySequence();
Performance Characteristics
- Memory Allocation: array allocations as chunks overflow, but size grows exponentially to upper limit
- Write Operations: O(1) amortized for sequential writes
- Sequence Access: O(n) to get
ReadOnlySequence<T> - Disposal: O(n) (returns arrays to pool)
(where n is number of memory chunks)
Configuration
Chunk Growth Strategy
The writer starts at defaultChunkBytes worth of elements and doubles on each allocation until it
reaches maxChunkBytes worth:
// Start at 1 KiB per chunk, grow to at most 16 KiB — whatever T is
using var writer = new ArrayPoolBufferWriter<byte>(
defaultChunkBytes: 1024,
maxChunkBytes: 16384
);
A maxChunkBytes at or below defaultChunkBytes is a valid way to say never grow rather than a
mistake — the ramp is skipped entirely and every chunk stays at the default size:
// Every chunk is exactly 512 bytes' worth of elements
using var writer = new ArrayPoolBufferWriter<byte>(
defaultChunkBytes: 512,
maxChunkBytes: 0
);
Array Clearing
For sensitive data, enable array clearing before returning to pool:
using var writer = new ArrayPoolBufferWriter<byte>(
clearArray: true,
defaultChunkBytes: 4096,
maxChunkBytes: 65536
);
Thread Safety
⚠️ Not thread-safe. External synchronization required for concurrent access.
Best Practices
DO: Dispose Properly
using var writer = new ArrayPoolBufferWriter<byte>();
// Use writer...
// Automatically disposed and arrays returned
DO: Provide Size Hints
// If you know the size, provide a hint
Span<byte> span = writer.GetSpan(sizeHint: 1024);
DON'T: Use ReadOnlySequence After More Writes
var sequence1 = writer.GetReadOnlySequence();
writer.GetSpan(100); // This invalidates sequence1!
DO: Get Sequence Once at the End
// Write all data
WriteData(writer);
// Get sequence once at the end
ReadOnlySequence<byte> finalSequence = writer.GetReadOnlySequence();
DO: Lease When the Payload Must Leave the Scope
static SequenceLease<byte> BuildPayload()
{
var writer = ObjectPools.RentBufferWriter<byte>();
Serialize(writer);
return writer.LeaseSequence(); // 0 bytes; the writer rides along
}
Choosing Between the Two
| API | Cost | The producer | Use when |
|---|---|---|---|
GetReadOnlySequence() |
0 B | you hold and dispose it yourself | consumption is inside the writer's scope |
LeaseSequence() |
0 B | rides along; freed with the lease | the payload leaves the scope |
Note the writer must not be disposed in the scope that leases it — the lease owns it now, and disposing it there would release the very buffers the payload is made of.
DON'T: Touch a Pooled Writer After Disposing It
using var writer = ObjectPools.RentBufferWriter<byte>();
// ...
writer.Dispose();
writer.GetSpan(16); // No exception — you are now writing into someone else's buffer
A returned instance does not throw ObjectDisposedException; it silently becomes whatever the next
renter is doing. This is the same contract as ArrayPool<T> itself.
Comparison with Alternatives
| Approach | Allocations | LOH Pressure | Complexity |
|---|---|---|---|
List<T> + ToArray() |
High | High for large data | Low |
MemoryStream |
Medium | Medium | Low |
ArrayPoolBufferWriter<T> |
Low | Low | Medium |
| Manual pooling | Lowest | Lowest | High |
Pooling the Writer Itself
The buffers are pooled by default; the writer object is not, unless you rent one. Declare a profile per use case and every one of them draws from the same pool:
static readonly ArrayPoolBufferWriterProvider<byte> JsonWriters = new(maxChunkBytes: 1 << 20);
using var writer = JsonWriters.Rent();
// ... write ...
// Disposing returns the writer to the pool, reset and ready for the next renter
For the default settings, ObjectPools.RentBufferWriter<T>() is the shorthand.
See ArrayPoolBufferWriterProvider<T> for why configuration is
applied at rent time rather than at construction, and what that means for clearArray.
See Also
- ArrayPoolBufferWriterProvider<T>
- ISequenceOwner<T>
- ArrayPoolMemoryStream
- ObjectPools
- IBufferWriter<T> Documentation
- Memory Package Overview
© 2026 The Keepers of the CryptoHives