Explain object pooling and when to use it.

4 minintermediate.NETobject-poolingperformancememory

Quick Answer

Object pooling reuses a set of pre-created objects instead of repeatedly allocating and collecting them, reducing GC pressure and allocation cost for expensive-to-create or frequently-used objects. .NET provides `ObjectPool<T>` and `ArrayPool<T>` for this. Use it for hot paths with high churn (buffers, large arrays, reusable workers); avoid it for cheap objects where pooling overhead and lifetime-management complexity outweigh the benefit.

Detailed Answer

Object Pooling is a design pattern that reuses objects instead of creating and destroying them repeatedly, reducing garbage collection pressure and improving performance.

How It Works:

  1. Create a pool of pre-initialized objects
  2. Rent an object when needed
  3. Return the object to the pool when done
  4. Reset the object state before returning to pool

Implementation Example:

// Using ObjectPool from Microsoft.Extensions.ObjectPool
public class BufferPool
{
    private readonly ObjectPool _pool;
    
    public BufferPool()
    {
        var policy = new DefaultPooledObjectPolicy
        {
            MaximumRetained = 100
        };
        
        _pool = new DefaultObjectPool(
            new BufferPoolPolicy(), 
            100
        );
    }
    
    public byte[] Rent()
    {
        return _pool.Get();
    }
    
    public void Return(byte[] buffer)
    {
        Array.Clear(buffer, 0, buffer.Length);
        _pool.Return(buffer);
    }
}

public class BufferPoolPolicy : IPooledObjectPolicy
{
    public byte[] Create()
    {
        return new byte[4096];
    }
    
    public bool Return(byte[] obj)
    {
        Array.Clear(obj, 0, obj.Length);
        return true;
    }
}

Using ArrayPool<T>:

public void ProcessData()
{
    // Rent array from pool
    byte[] buffer = ArrayPool.Shared.Rent(1024);
    
    try
    {
        // Use the buffer
        ReadData(buffer);
        ProcessBuffer(buffer);
    }
    finally
    {
        // Always return to pool
        ArrayPool.Shared.Return(buffer, clearArray: true);
    }
}

Custom Object Pool:

public class Connection
{
    public void Open() { }
    public void Close() { }
    public void Reset() { }
}

public class ConnectionPool
{
    private readonly ConcurrentBag _pool = new();
    private readonly int _maxSize;
    private int _count;
    
    public ConnectionPool(int maxSize)
    {
        _maxSize = maxSize;
    }
    
    public Connection Rent()
    {
        if (_pool.TryTake(out var connection))
        {
            return connection;
        }
        
        if (_count < _maxSize)
        {
            Interlocked.Increment(ref _count);
            return new Connection();
        }
        
        // Wait for available connection
        SpinWait.SpinUntil(() => _pool.TryTake(out connection));
        return connection;
    }
    
    public void Return(Connection connection)
    {
        connection.Reset();
        _pool.Add(connection);
    }
}

When to Use Object Pooling:

Use When:

  • Objects are expensive to create (database connections, large arrays)
  • Objects are created and destroyed frequently
  • Garbage collection pressure is high
  • Application has predictable object usage patterns
  • Objects can be safely reused after resetting state

Good Candidates:

  • Database connections
  • HTTP client instances
  • Large buffers or arrays
  • StringBuilder instances
  • MemoryStream objects
  • Socket connections
  • Encryption/hashing objects

Don't Use When:

  • Objects are lightweight and cheap to create
  • Object creation rate is low
  • Objects cannot be safely reset or reused
  • Pool management overhead exceeds creation cost
  • Objects have complex state that's hard to reset

Benefits:

  • Reduced GC pressure
  • Improved performance
  • Lower memory allocation
  • Predictable memory usage
  • Faster object acquisition

Considerations:

  • Thread safety
  • Proper cleanup/reset of pooled objects
  • Pool size management
  • Memory leaks if objects aren't returned
  • Potential for holding onto memory longer than needed

Example Use Case:

public class ImageProcessor
{
    private readonly ObjectPool _stringBuilderPool;
    private readonly ArrayPool _byteArrayPool;
    
    public async Task ProcessImageAsync(Stream imageStream)
    {
        // Rent pooled objects
        var sb = _stringBuilderPool.Get();
        var buffer = _byteArrayPool.Rent(8192);
        
        try
        {
            int bytesRead;
            while ((bytesRead = await imageStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                // Process buffer
                sb.Append(Convert.ToBase64String(buffer, 0, bytesRead));
            }
            
            return sb.ToString();
        }
        finally
        {
            // Always return to pools
            sb.Clear();
            _stringBuilderPool.Return(sb);
            _byteArrayPool.Return(buffer, clearArray: true);
        }
    }
}

Built-in .NET Pools:

  • ArrayPool<T>.Shared
  • MemoryPool<T>.Shared
  • ObjectPool<T> (Microsoft.Extensions.ObjectPool)
  • HttpClient connection pooling (automatic)
  • Thread pool