How ArrayPool Reduces GC Pressure in High-Throughput C

This article explores ArrayPool<T> in .NET, a high-performance memory pooling mechanism designed to minimize garbage collection (GC) overhead in demanding applications. You will learn the mechanics behind array allocations on the managed heap, how pooling mitigates Gen 0 collections and Large Object Heap (LOH) fragmentation, how to properly rent and return buffers, and essential best practices for safe implementation.

The Cost of Frequent Array Allocations

In high-throughput C# applications—such as web servers, real-time message processors, and networking pipelines—allocating temporary byte buffers or object arrays happens constantly. Every new byte[size] allocates memory on the managed heap. Small, short-lived arrays quickly fill Generation 0, triggering frequent Gen 0 and Gen 1 garbage collections.

When arrays exceed 85,000 bytes, the .NET runtime allocates them directly onto the Large Object Heap (LOH). The LOH is not compacted by default during standard garbage collection cycles, which leads to heap fragmentation and expensive Generation 2 (full) collections. Frequent full GC pauses increase response latencies and significantly degrade throughput.

What is ArrayPool<T>?

Introduced in System.Buffers, ArrayPool<T> is a thread-safe implementation of the object pool pattern specifically designed for arrays. Instead of creating and discarding array instances, an application rents an array from the pool, uses it for a transient operation, and returns it when finished.

Because the underlying arrays persist inside the pool across multiple operations, the runtime does not need to continuously allocate and deallocate heap memory.

How to Use ArrayPool<T>

The most common way to access an array pool is through the shared default instance: ArrayPool<T>.Shared.

The standard lifecycle involves three steps:

  1. Rent: Request an array of a minimum size.
  2. Execute: Perform work using the rented buffer.
  3. Return: Place the buffer back in the pool inside a finally block.
using System;
using System.Buffers;

public void ProcessPayload(ReadOnlySpan<byte> incomingData)
{
    // Rent an array with at least the required capacity
    byte[] buffer = ArrayPool<byte>.Shared.Rent(incomingData.Length);

    try
    {
        // Copy data into the buffer and perform work
        incomingData.CopyTo(buffer);
        PerformWork(buffer, incomingData.Length);
    }
    finally
    {
        // Always return the array to the pool
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

Key Mechanics and Behaviors

1. Rented Arrays May Be Larger Than Requested

ArrayPool<T>.Rent(int minimumLength) guarantees an array that has at least the requested size, but it often returns a larger array. The pool groups arrays into power-of-two buckets (e.g., renting 1,000 bytes might return a 1,024-byte array). Your logic must track the actual number of valid bytes rather than relying on buffer.Length.

2. Cleaning Data on Return

The Return(T[] array, bool clearArray = false) method accepts an optional boolean:

  • clearArray: false (Default): Leaves data intact. This provides the highest performance and is safe for value types like byte or int when no sensitive data (passwords, tokens) is involved.
  • clearArray: true: Overwrites the array with default values before storing it. This is mandatory for arrays containing reference types to prevent object references from lingering and delaying GC collection of referenced instances, or when sensitive information must be cleared from memory.

Impact on Garbage Collection

By substituting dynamic allocations with rented buffers, ArrayPool<T> alters application performance in three key areas:

  • Elimination of Allocation-Driven GC: Reusing a pre-allocated set of buffers stops continuous allocation cycles, keeping Gen 0 collection counts near zero during steady-state processing.
  • Mitigation of LOH Fragmentation: Large buffers rented from ArrayPool<T> remain resident in Gen 2/LOH, eliminating the cycle of allocating and abandoning large blocks of contiguous memory.
  • Reduced CPU Overhead: Because the CPU spends less time scanning, compacting, and collecting memory, more execution cycles remain available for application logic.

Critical Safety Rules

  • Always use try...finally: Failing to return an array leads to memory leaks inside the pool, forcing the pool to allocate new arrays to replace lost ones.
  • Do not use after return: Accessing an array after returning it introduces severe race conditions and data corruption, as another thread may be actively writing to it.
  • Do not return twice: Returning the same array reference multiple times will corrupt the internal bucket states of the pool.