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
GetSpan
andGetMemory
for 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;
}
}
}