Pooling: ArrayPool and friends
When pooling pays, how ArrayPool buckets work, and the bugs renting/returning introduce.
Pooling: ArrayPool and friends
ArrayPool<T>.Shared rents byte/T arrays from size-classed buckets
(powers of two; you may get a LARGER array than requested โ always use
your logical length, never rented.Length). Rent/Return are cheap
interlocked ops; the win shows on hot paths allocating big buffers per
request.
byte[] buf = ArrayPool<byte>.Shared.Rent(minLength: 4096);
try { Use(buf.AsSpan(0, logical)); }
finally { ArrayPool<byte>.Shared.Return(buf); } // clearArray: true only for secrets
The four bugs pooling introduces (why it is opt-in, not default):
- Forgetting Return โ the buffer leaks from the pool (not the GC); the pool grows, memory balloons.
- Use-after-return โ a Span held past Return reads another renter's data. Correctness bug, possibly a security bug (cross-request data bleed).
- Wrong length assumptions โ rented.Length โฅ your need; slicing is mandatory.
- Returning dirty buffers with secrets โ
clearArray: truewhen the content was sensitive.
When pooling PAYS: large (โฅKBs) buffers on request-rate hot paths.
When it does not: small short-lived allocations (Gen0 is cheaper than
the API ceremony), anything rent/return dominated by work smaller than
the allocation itself. Measure with GC.GetAllocatedBytesForCurrentThread
before and after โ the checkpoint's second test enforces this.
ObjectPool<T> (Microsoft.Extensions.ObjectPool) does the same for
polymorphic objects โ same rules apply: rent, use, return, no leaks past
return.