hits counter

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 and GetMemory 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;
        }
    }
}

No Comments

Add a Comment

As it will appear on the website

Not displayed

Your website