Efficient Byte Buffering in .NET with PooledBuffer
🚀 Efficient Byte Buffering in .NET with
PooledBuffer
When working with high-performance applications—like network
servers, serialization libraries, or streaming
systems—efficient memory management is crucial. One powerful
technique is buffer pooling, which reduces
allocations and garbage collection pressure by reusing
memory. In this post, we’ll explore a custom utility class
called PooledBuffer, which leverages
ArrayPool<byte> and exposes a flexible,
efficient way to write and read byte data.
🔍 What Is PooledBuffer?
PooledBuffer is a custom implementation of
IBufferWriter<byte> that uses
ArrayPool<byte>.Shared to rent and manage
byte arrays. It supports:
- Dynamic growth: Starts with an initial buffer and adds more as needed.
-
Efficient writing: Implements
GetSpanandGetMemoryfor writing data. -
Read access: Exposes a
ReadOnlySequence<byte>for reading the written data. - Memory reuse: Returns buffers to the pool on disposal.
This makes it ideal for scenarios where you need to write a stream of bytes and later read them efficiently—without allocating new arrays every time.
🛠️ How It Works
1. Writing Data
The class implements IBufferWriter<byte>,
which provides two methods:
GetSpan(int sizeHint = 0)GetMemory(int sizeHint = 0)
These methods ensure that the current buffer has enough
space. If not, a new buffer is rented from the pool and
added to the internal list. After writing,
Advance(int count) is called to notify how many
bytes were written.
2. Reading Data
After writing, you can retrieve the data as a
ReadOnlySequence<byte> using
GetReadOnlySequence(). This method stitches
together all the written segments into a single sequence,
ideal for parsers or pipelines.
3. Disposal and Cleanup
When you're done, call Dispose(). This returns
all rented buffers to the pool and clears internal state,
ensuring memory is reused efficiently.
📦 Use Cases
Here are some real-world scenarios where
PooledBuffer shines:
âś… Serialization Libraries
Write serialized data into a pooled buffer and expose it as
a ReadOnlySequence<byte> for transmission
or storage.
âś… Network Protocols
Accumulate incoming data into a pooled buffer, then parse it
using a ReadOnlySequence<byte>.
âś… Streaming APIs
Write chunks of data as they arrive, and later read them as a contiguous sequence without copying.
âś… Custom Pipelines
Use it as a lightweight alternative to
PipeWriter when you don’t need full duplex or
backpressure support.
đź§ Why Not Just Use MemoryStream?
While MemoryStream is convenient, it:
- Allocates a single large array that grows via copying.
- Doesn’t support ReadOnlySequence
. - Doesn’t return memory to a pool.
PooledBuffer avoids these issues by pooling
memory and supporting segmented reads.
đź§Ş Example Usage
var buffer = new PooledBuffer();
var span = buffer.GetSpan(100);
Encoding.UTF8.GetBytes("Hello, world!", span);
buffer. Advance(13);
ReadOnlySequence<byte> sequence = buffer.GetReadOnlySequence();
// Use sequence with a parser or writer
buffer.Dispose(); // Return memory to the pool
đź§Ľ Final Thoughts
PooledBuffer is a powerful utility for
developers working with byte streams in performance-critical
.NET applications. It combines:
-
Efficient memory reuse via
ArrayPool<byte> -
Flexible writing through
IBufferWriter<byte> -
Seamless reading with
ReadOnlySequence<byte>
Whether you're building serializers, network protocols, or
custom pipelines, PooledBuffer offers a
lightweight and effective solution for managing byte data.
đź§© Complete Class Code
Below is the full implementation of the
PooledBuffer class. This code brings together
all the concepts discussed above—buffer pooling, dynamic
growth, efficient writing, and segmented reading via
ReadOnlySequence<byte>. You can use this
class as-is or adapt it to fit the specific performance
needs of your application.
/// <summary>
/// A pooled buffer writer that implements <see cref="IBufferWriter{Byte}"/> using <see cref="ArrayPool{Byte}.Shared"/> for efficient writing of byte data and
/// allows reading the written content through a <see cref="ReadOnlySequence{Byte}"/> using the <see cref="GetReadOnlySequence"/> method.
/// </summary>
public sealed class PooledBuffer : IBufferWriter<byte>, IDisposable
{
private const int DefaultBufferSize = 4096;
private readonly List<byte[]> _buffers = new();
private readonly ArrayPool<byte> _pool;
private int _currentIndex;
private int _currentOffset;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="PooledBuffer"/> class with an optional initial buffer size and array pool.
/// </summary>
/// <param name="initialBufferSize">The initial size of the buffer to rent from the pool. Defaults to 4096 bytes.</param>
/// <param name="pool">The array pool to use. If null, <see cref="ArrayPool{Byte}.Shared"/> is used.</param>
public PooledBuffer(int initialBufferSize = DefaultBufferSize, ArrayPool<byte>? pool = null)
{
_pool = pool ?? ArrayPool<byte>.Shared;
AddNewBuffer(initialBufferSize);
}
/// <summary>
/// Notifies the buffer writer that <paramref name="count"/> bytes were written.
/// </summary>
/// <param name="count">The number of bytes written.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if count is negative or exceeds the current buffer capacity.</exception>
public void Advance(int count)
{
if (count < 0 || _currentOffset + count > _buffers[_currentIndex].Length)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
_currentOffset += count;
}
/// <summary>
/// Returns a <see cref="Memory{Byte}"/> buffer to write to, ensuring at least <paramref name="sizeHint"/> bytes are available.
/// </summary>
/// <param name="sizeHint">The minimum number of bytes required. May be 0.</param>
/// <returns>A writable memory buffer.</returns>
public Memory<byte> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffers[_currentIndex].AsMemory(_currentOffset);
}
/// <summary>
/// Returns a <see cref="Span{Byte}"/> buffer to write to, ensuring at least <paramref name="sizeHint"/> bytes are available.
/// </summary>
/// <param name="sizeHint">The minimum number of bytes required. May be 0.</param>
/// <returns>A writable span buffer.</returns>
public Span<byte> GetSpan(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffers[_currentIndex].AsSpan(_currentOffset);
}
/// <summary>
/// Returns a <see cref="ReadOnlySequence{Byte}"/> representing the written data across all buffers.
/// </summary>
/// <returns>A read-only sequence of bytes.</returns>
public ReadOnlySequence<byte> GetReadOnlySequence()
{
SequenceSegment? first = null;
SequenceSegment? last = null;
for (var i = 0; i < _buffers.Count; i++)
{
var buffer = _buffers[i];
var length = (i == _currentIndex) ? _currentOffset : buffer.Length;
if (length == 0)
{
continue;
}
var segment = new SequenceSegment(buffer.AsMemory(0, length));
if (first == null)
{
first = segment;
}
if (last != null)
{
last.SetNext(segment);
}
last = segment;
}
if (first == null || last == null)
{
return ReadOnlySequence<byte>.Empty;
}
return new ReadOnlySequence<byte>(first, 0, last, last.Memory.Length);
}
/// <summary>
/// Releases all buffers back to the pool and clears internal state.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var buffer in _buffers)
{
_pool.Return(buffer);
}
_buffers.Clear();
_disposed = true;
}
private void EnsureCapacity(int sizeHint)
{
if (_currentOffset + sizeHint > _buffers[_currentIndex].Length)
{
var newSize = Math.Max(sizeHint, DefaultBufferSize);
AddNewBuffer(newSize);
}
}
private void AddNewBuffer(int size)
{
var buffer = _pool.Rent(size);
_buffers.Add(buffer);
_currentIndex = _buffers.Count - 1;
_currentOffset = 0;
}
private class SequenceSegment : ReadOnlySequenceSegment<byte>
{
public SequenceSegment(ReadOnlyMemory<byte> memory)
{
Memory = memory;
}
public void SetNext(SequenceSegment next)
{
Next = next;
next.RunningIndex = RunningIndex + Memory.Length;
}
}
}