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:
- Rent: Request an array of a minimum size.
- Execute: Perform work using the rented buffer.
- Return: Place the buffer back in the pool inside a
finallyblock.
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 likebyteorintwhen 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.