High-performance, thread-safe object pool for reference types in .NET. Optimized for ultra-low overhead under contention with per-thread fast paths, avoiding locks on hot paths.
Under concurrent workloads, Tedd.ObjectPool delivers between 11× and 111× higher throughput than Microsoft.Extensions.ObjectPool, with the largest gains at high thread counts.
- .NET CLI:
dotnet add package Tedd.ObjectPool
- PackageReference:
<ItemGroup> <PackageReference Include="Tedd.ObjectPool" Version="x.y.z" /> </ItemGroup>
- Package Manager Console:
Install-Package Tedd.ObjectPool -Version x.y.z
Replace x.y.z with the desired version.
Basic pooling of an object, e.g., StringBuilder:
using Tedd;
using System.Text;
var pool = new ObjectPool<StringBuilder>(
factory: () => new(capacity: 256),
cleanup: sb => sb.Clear(), // reset before publishing back
size: 64 // total pool slots
);
var sb = pool.Allocate();
try
{
sb.Append("Hello, world!");
// ... use sb ...
}
finally
{
pool.Free(sb); // cleanup runs, then it goes back to the pool
}-
Constructors
new ObjectPool<T>(Factory factory)new ObjectPool<T>(Factory factory, int size)new ObjectPool<T>(Factory factory, Action<T> cleanup, int size)new ObjectPool<T>(Factory factory, Action<T>? cleanup, int size, bool disposeWhenFull)
Where
Factoryisdelegate T Factory()andT : class. -
Core methods
T Allocate()void Free(T obj)(no-op ifobjisnull)void Prefill(int count)– optionally pre-create items to reduce cold-start latencyvoid AllocateExecuteDeallocate(Action<T> action, Action<T>? cleanupAction = null)void Scoped<TState>(TState state, Action<T,TState> action)– allocation-free scoped usagevoid Dispose()– disposes internal TLS cache; pooled items are not disposed
-
Diagnostics (DEBUG builds)
void ForgetTrackedObject(T old, T? replacement = null)– mark an object as intentionally not returned
- Default size is
Math.Max(1, Environment.ProcessorCount * 2)when only the factory is provided. - If
Freeis called when the pool is full:- Default: the object is dropped (eligible for GC).
- If constructed with
disposeWhenFull: trueandT : IDisposable, the object is disposed.
- The optional
cleanupaction runs on everyFreebefore the object is published back to the pool. Free(null)is a no-op; useful for simplifying caller logic.
var pool = new ObjectPool<byte[]>(
factory: () => new byte[4096],
cleanup: _ => { /* optional reset */ },
size: 128
);
var buffer = pool.Allocate();
try
{
// use buffer
}
finally
{
pool.Free(buffer);
}pool.Scoped(Unit.Value, (sb, _) =>
{
sb.AppendLine("Scoped work");
});
// Or with state:
pool.Scoped("hello", (sb, text) =>
{
sb.Append(text);
});
readonly struct Unit { public static readonly Unit Value = default; }pool.AllocateExecuteDeallocate(
action: sb => { sb.Append("work"); },
cleanupAction: sb => sb.Clear() // optional extra cleanup for this call
);pool.Prefill(count: 32);var socketPool = new ObjectPool<System.Net.Sockets.Socket>(
factory: () => new(System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp),
cleanup: s => { /* reset if applicable */ },
size: 32,
disposeWhenFull: true // overflowed sockets are disposed instead of dropped
);- Designed for multi-threaded use. Most hot-path operations avoid locks.
- A per-thread single-item cache greatly reduces interlocked traffic for common allocate/free pairs.
- Allocation attempts spread contention across slots using rotating indices.
- Reuse of expensive-to-create reference types (e.g.,
StringBuilder, buffers, serializers,MemoryStream). - High-throughput services needing minimal allocation and contention.
Tmust be a reference type (class).- The pool does not own lifetime of items except when
disposeWhenFull: trueis enabled for overflow. Dispose()only tears down internal thread-local storage; it does not dispose pooled items.
Tedd.ObjectPool utilizes a multi-tiered allocation strategy designed to minimize lock contention and interlocked operations on hot paths:
- Fast Slot (
_firstItem): If the TLS cache is empty during allocation, the pool attempts an optimistic read and a single CAS operation against a dedicated, highly-contended "fast slot". During deallocation (when TLS is already occupied), the pool publishes to the fast slot usingVolatile.Read/Volatile.Writewhen it is observed empty. - Factory Fallback / Overflow: If the array is exhausted during allocation, a new instance is instantiated via the provided delegate. During deallocation, if the pool is at maximum capacity, the object is either dropped for garbage collection or explicitly disposed (if
disposeWhenFullis configured and the type implementsIDisposable).
Note: The architecture described above reflects the current implementation. There are currently no speculative future enhancements (hypotheses) planned for the core execution flow.
- Fast paths kept tiny:
Allocate/Freeare aggressively inlined; slow paths are markedNoInliningto keep the I-cache hot. - Per-thread 1-slot cache: Most allocate/free pairs complete without touching shared memory.
- Fast slot + array: One hot fast slot (
_firstItem) plus an array of elements (size - 1). - Correct memory publication: Uses
Volatile.Read/Writefor unsynchronized accesses and a single CAS (Interlocked.CompareExchange) to claim/release. - Contention spreading: Rotating probe indices on allocate/free reduce CAS collisions and cache-line ping-pong.
- Optional prefill:
Prefillreduces first-use latency when the factory is expensive. - Overflow policy: Configurable disposal of
IDisposableitems when the pool is full; default is drop-on-floor to let GC reclaim. - DEBUG diagnostics: Optional leak tracking warns via
Debug.WriteLineif an allocated item is never returned;ForgetTrackedObjectlets you intentionally replace an item (e.g., upsizing a buffer) without spurious warnings.
These choices aim to deliver predictable, low-latency operation under contention while keeping the API simple and allocation-free on the hot path.
Tedd.ObjectPool is consistently faster than Microsoft.Extensions.ObjectPool across all tested thread counts.
- 2 threads: Tedd.ObjectPool is 11.28× faster
- 4 threads: 19.12× faster
- 8 threads: 36.30× faster
- 16 threads: ~95× faster
- 32 threads: ~111× faster
- Tedd.ObjectPool sustains low, stable latencies as concurrency increases (8.8 µs → 35.9 µs).
- The performance advantage grows with thread count, indicating better scalability and reduced contention effects.
Under concurrent workloads, Tedd.ObjectPool delivers between 11× and 111× higher throughput than Microsoft.Extensions.ObjectPool, with the largest gains at high thread counts.
// * Summary *
BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4652/24H2/2024Update/HudsonValley) Unknown processor .NET SDK 10.0.100-preview.6.25358.103 [Host] : .NET 9.0.7 (9.0.725.31616), X64 RyuJIT AVX2 DefaultJob : .NET 9.0.7 (9.0.725.31616), X64 RyuJIT AVX2
| Method | Threads | OperationsPerThread | BufferSize | PoolSize | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Completed Work Items | Lock Contentions | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tedd.ObjectPool | 2 | 1000 | 256 | 64 | 8.827 us | 0.1760 us | 0.1646 us | 8.865 us | 1.00 | 0.03 | 0.0916 | 1.0000 | 0.0000 | - | 1.66 KB | 1.00 |
| Microsoft.Extensions.ObjectPool | 2 | 1000 | 256 | 64 | 99.532 us | 1.8606 us | 1.7404 us | 99.644 us | 11.28 | 0.28 | - | 1.0000 | 0.0001 | - | 1.66 KB | 1.00 |
| Tedd.ObjectPool_v1 | 2 | 1000 | 256 | 64 | 40.741 us | 0.7680 us | 0.7184 us | 40.656 us | 4.62 | 0.11 | 2.1973 | 1.0000 | 0.0002 | - | 36.06 KB | 21.77 |
| Tedd.ObjectPool | 4 | 1000 | 256 | 64 | 14.222 us | 0.4897 us | 1.3893 us | 14.608 us | 1.01 | 0.17 | 0.1221 | 2.9998 | 0.0002 | - | 2.08 KB | 1.00 |
| Microsoft.Extensions.ObjectPool | 4 | 1000 | 256 | 64 | 268.482 us | 3.0884 us | 4.4294 us | 267.717 us | 19.12 | 2.55 | - | 2.9995 | 0.0054 | - | 2.1 KB | 1.01 |
| Tedd.ObjectPool_v1 | 4 | 1000 | 256 | 64 | 157.382 us | 2.5749 us | 3.1622 us | 156.239 us | 11.21 | 1.50 | 15.3809 | 2.9998 | 0.0027 | - | 248.64 KB | 119.65 |
| Tedd.ObjectPool | 8 | 1000 | 256 | 64 | 17.623 us | 0.3764 us | 1.1098 us | 17.864 us | 1.01 | 0.10 | 0.1678 | 6.9634 | 0.0003 | - | 2.92 KB | 1.00 |
| Microsoft.Extensions.ObjectPool | 8 | 1000 | 256 | 64 | 636.495 us | 8.2262 us | 10.1025 us | 633.216 us | 36.30 | 2.97 | - | 6.9971 | 0.0029 | - | 2.98 KB | 1.02 |
| Tedd.ObjectPool_v1 | 8 | 1000 | 256 | 64 | 438.754 us | 4.0612 us | 6.8961 us | 436.242 us | 25.02 | 2.05 | 51.7578 | 6.9990 | 0.0039 | - | 844.28 KB | 289.43 |
| Tedd.ObjectPool | 16 | 1000 | 256 | 64 | 21.821 us | 0.3818 us | 0.8695 us | 21.776 us | 1.00 | 0.05 | 0.2441 | 12.0614 | 0.0002 | - | 4.16 KB | 1.00 |
| Microsoft.Extensions.ObjectPool | 16 | 1000 | 256 | 64 | 2,068.935 us | 39.3110 us | 46.7970 us | 2,086.189 us | 94.95 | 4.14 | - | 15.0000 | 0.0078 | - | 4.71 KB | 1.13 |
| Tedd.ObjectPool_v1 | 16 | 1000 | 256 | 64 | 1,089.897 us | 18.9815 us | 30.6516 us | 1,078.430 us | 50.02 | 2.34 | 119.1406 | 14.9629 | 0.0117 | 1.9531 | 1955.6 KB | 470.63 |
| Tedd.ObjectPool | 32 | 1000 | 256 | 64 | 35.892 us | 0.1476 us | 0.1232 us | 35.851 us | 1.00 | 0.00 | 0.3052 | 16.7516 | 0.0001 | - | 5.78 KB | 1.00 |
| Microsoft.Extensions.ObjectPool | 32 | 1000 | 256 | 64 | 3,974.344 us | 69.3458 us | 64.8661 us | 4,003.417 us | 110.73 | 1.79 | - | 30.9922 | 0.0156 | - | 8.23 KB | 1.42 |
| Tedd.ObjectPool_v1 | 32 | 1000 | 256 | 64 | 2,798.889 us | 21.8024 us | 19.3273 us | 2,793.363 us | 77.98 | 0.58 | 253.9063 | 30.5508 | 0.0156 | 7.8125 | 4137.25 KB | 716.00 |