Prest.SystemTextJson.ObjectPool 0.1.2

dotnet add package Prest.SystemTextJson.ObjectPool --version 0.1.2
                    
NuGet\Install-Package Prest.SystemTextJson.ObjectPool -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Prest.SystemTextJson.ObjectPool" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Prest.SystemTextJson.ObjectPool" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Prest.SystemTextJson.ObjectPool" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Prest.SystemTextJson.ObjectPool --version 0.1.2
                    
#r "nuget: Prest.SystemTextJson.ObjectPool, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Prest.SystemTextJson.ObjectPool@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Prest.SystemTextJson.ObjectPool&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Prest.SystemTextJson.ObjectPool&version=0.1.2
                    
Install as a Cake Tool

Prest

NuGet Downloads CI License: MIT

Zero-allocation pooled collections for .NET with pluggable, zero-cost hash-table algorithms.

Highlights

  • Pooled backing store. Arrays come from ArrayPool<T>.Shared; Dispose returns them. Single-consumer ownership — no locking, no concurrent mutation.
  • Pluggable hash algorithm. PooledHashMap<K,V> and PooledHashSet<T> pick a probe strategy at the type-parameter level: Swiss (SIMD group scan; default, matches .NET 9's Dictionary), RobinHood (displacement-tracking linear, backward-shift on erase — no tombstones), Linear (simplest), Chained (dense entry storage, iteration-friendly). The algorithm is a struct generic — the JIT monomorphizes every call end-to-end. No virtual dispatch, no boxing.
  • Auto-return-on-Dispose. PooledHashMap<K,V>.Create() rents from a per-thread cache; using disposes back into that cache so the next rental on the same thread skips allocation entirely.
  • Concurrent pool for await code. Prest.ObjectPool ships PooledHashMapPool<K,V> / PooledHashSetPool<T> / PooledBufferWriterPool<T> backed by Microsoft.Extensions.ObjectPool.DefaultObjectPool — safe across threads, survives await, Dispose auto-returns to the pool.
  • Zero-allocation enumeration. foreach, .Keys, .Values all return value-type enumerators — no IEnumerator<T> boxing.
  • Serializer integrations. System.Text.Json converters and VYaml formatters for PooledArray<T> and PooledHashMap<K,V>.
  • Native AOT-compatible. All shipping packages build with IsAotCompatible=true on .NET 8+. The reflection-based factory / resolver are flagged with RequiresDynamicCode so AOT analyzers point you at the concrete per-type converter/formatter as the AOT-safe alternative.

Packages

Package Version Source Purpose
Prest NuGet src/Prest Core — PooledHashMap<K,V>, PooledHashSet<T>, algorithm/finalizer/hasher structs, PooledArray<T>, PooledList<T>, PooledBufferWriter<T>
Prest.ObjectPool NuGet src/Prest.ObjectPool DefaultObjectPool-backed PooledHashMapPool<K,V>, PooledHashSetPool<T>, PooledBufferWriterPool<T> for await-safe rent/return
Prest.SystemTextJson NuGet src/Prest.SystemTextJson PooledJsonBufferWriter (pairs Utf8JsonWriter with PooledBufferWriter<byte>) + threadstatic cache
Prest.SystemTextJson.ObjectPool NuGet src/Prest.SystemTextJson.ObjectPool DefaultObjectPool-backed accessor for PooledJsonBufferWriter
Prest.Serializers.SystemTextJson NuGet src/Prest.Serializers.SystemTextJson JsonConverter implementations + PooledTypesJsonConverterFactory (one registration covers PooledArray<T>, PooledList<T>, PooledHashMap<K,V>, PooledHashSet<T> for any algorithm)
Prest.Serializers.VYaml NuGet src/Prest.Serializers.VYaml IYamlFormatter implementations for the same four types + PooledTypeFormatterResolver
Prest.Immutable NuGet src/Prest.Immutable PooledArray<T>ImmutableArray<T> conversion extensions

Install

Core package — pooled collections, PooledHashMap, PooledHashSet:

dotnet add package Prest

Optional packages, install only what you need:

# Await-safe object pools (PooledHashMapPool, PooledHashSetPool, PooledBufferWriterPool)
dotnet add package Prest.ObjectPool

# System.Text.Json integration — PooledJsonBufferWriter
dotnet add package Prest.SystemTextJson
dotnet add package Prest.SystemTextJson.ObjectPool   # + await-safe accessor

# JsonConverter / IYamlFormatter for pooled collection types
dotnet add package Prest.Serializers.SystemTextJson
dotnet add package Prest.Serializers.VYaml

# ImmutableArray<T> ↔ PooledArray<T> interop (net10.0-only)
dotnet add package Prest.Immutable

All packages target net10.0, net8.0, netstandard2.1, netstandard2.0 (except Prest.Serializers.SystemTextJson which is net8.0+ and Prest.Immutable which is net10.0-only). See Target frameworks for the full matrix.

Quick start

using Prest;

// `using` disposes the map and returns pooled arrays to ArrayPool.Shared.
using var map = PooledHashMap<int, string>.Create(capacity: 16);

map.Add(1, "one");
map.Add(2, "two");
map.Add(3, "three");

Console.WriteLine($"Count: {map.Count}");          // 3
Console.WriteLine($"map[2] = {map[2]}");            // two

if (map.TryGetValue(3, out var three))
{
    Console.WriteLine($"TryGetValue(3) -> {three}");
}

map.Remove(1);
Console.WriteLine($"ContainsKey(1): {map.ContainsKey(1)}");  // False

Tour

Hash map / hash set

The default PooledHashMap<K,V> / PooledHashSet<T> close over SwissTable — the same algorithm .NET 9's Dictionary moved to, chosen as a reasonable starting point for mixed workloads. Create(capacity) rents from a per-thread cache, Dispose returns to it. See Choosing an algorithm for when to pick a different one.

using var map = PooledHashMap<int, string>.Create(capacity: 16);
map.Add(1, "one");

using var set = PooledHashSet<string>.Create(capacity: 8);
set.Add("apple");
var wasNew = set.Add("apple");  // false — already present

Pick an algorithm

Same API surface on every alias — swap the type name, nothing else:

using var swiss  = SwissHashMap<int, int>.Create(32);
using var robin  = RobinHoodHashMap<int, int>.Create(32);
using var linear = LinearHashMap<int, int>.Create(32);
using var chain  = ChainedHashMap<int, int>.Create(32);

PooledHashMap<K,V> is an alias for SwissHashMap<K,V>. See Choosing an algorithm below.

Custom equality comparer

Each algorithm ships a Comparer{Algo}HashMap<K,V> / Comparer{Algo}HashSet<T> variant that takes a runtime IEqualityComparer<T>:

using var map = ComparerSwissHashMap<string, int>.Create(
    StringComparer.OrdinalIgnoreCase, capacity: 8);

map.Add("Hello", 1);
Console.WriteLine(map.ContainsKey("HELLO"));  // True

Custom hash finalizer

Bit-mixing stage applied after GetHashCode. Useful when keys have poor distribution (sequential ints, identity-hashed refs, single-field structs). Pick a finalizer via the second generic-arity alias:

// Sequential stride keys — Fibonacci spreads them out.
using var fib = SwissHashMap<int, int, FibonacciFinalizer>.Create(capacity: 64);
for (var i = 0; i < 32; i++) fib.Add(i * 1024, i);

// Adversarial input — XMX has the strongest mixing.
using var xmx = RobinHoodHashMap<int, int, XmxFinalizer>.Create(capacity: 64);

See Choosing a finalizer below.

Enumeration (zero allocation)

foreach, .Keys, and .Values return value-type enumerators — no boxing, no IEnumerator<T> allocation.

foreach (var kvp in map)             { /* kvp.Key, kvp.Value */ }
foreach (var key   in map.Keys)      { /* ... */ }
foreach (var value in map.Values)    { /* ... */ }

Pooled arrays and lists

// Collection-expression literal over ArrayPool.Shared.
using PooledArray<int> numbers = [1, 2, 3];

// Growable, heap-allocated wrapper.
using var list = new PooledList<int>(initialCapacity: 16);
list.Add(42);

// Struct variant — zero wrapper allocation. Copies share rented buffers; Dispose one.
using var value = new ValuePooledList<int>(16);
value.Add(7);

// Ref-struct variant — compiler-enforced stack-only; an inline Span<T>
// can hold items before the pool rental happens.
Span<int> inline = stackalloc int[8];
using var stack = new StackOnlyPooledList<int>(inline);
stack.Add(99);

IBufferWriter<T>

Same-thread reuse via a threadstatic cache:

var writer = PooledBufferWriter<byte>.Rent();
try
{
    var span = writer.GetSpan(128);
    // ... write bytes, call writer.Advance(n)
    ReadOnlyMemory<byte> bytes = writer.WrittenMemory;
}
finally
{
    PooledBufferWriter<byte>.Return(writer);
}

For pooling that survives await, install Prest.ObjectPool:

using Prest.ObjectPool;

var writer = PooledBufferWriterPool<byte>.Rent();
try
{
    await Task.Yield();    // writer is still valid on any continuation thread
    // ...
}
finally
{
    PooledBufferWriterPool<byte>.Return(writer);
}

JSON

using System.Text.Json;
using Prest;
using Prest.Serializers.SystemTextJson;

// One registration covers every pooled type for any algorithm closure.
var options = new JsonSerializerOptions
{
    Converters = { new PooledTypesJsonConverterFactory() },
};

PooledArray<int> values = [1, 2, 3];
string json = JsonSerializer.Serialize(values, options);
using var roundTripped = JsonSerializer.Deserialize<PooledArray<int>>(json, options);

Map keys must be IUtf8SpanFormattable (enforced by the converter's generic constraints). If you prefer to register converters individually — e.g. to restrict which closed types are supported — the factory-less form is still there:

options.Converters.Add(new PooledArrayConverter<int>());
options.Converters.Add(new PooledListConverter<int>());
options.Converters.Add(new PooledHashSetConverter<int,
    SwissTableAlgorithm<int, int, EqualityDefaultHasher<int>, NoOpHashFinalizer>>());
options.Converters.Add(new PooledHashMapConverter<int, string,
    SwissTableAlgorithm<KeyValueSlot<int, string>, int, EqualityDefaultHasher<int>, NoOpHashFinalizer>>());

YAML

using VYaml.Serialization;
using Prest;
using Prest.Serializers.VYaml;

var options = YamlSerializerOptions.Standard;
options.Resolver = CompositeResolver.Create(
    (IYamlFormatterResolver[])
    [
        PooledTypeFormatterResolver.Instance,
        StandardResolver.Instance,
    ]);

PooledArray<string> items = ["alpha", "beta"];
string yaml = YamlSerializer.SerializeToString(items, options);

The resolver covers PooledArray<T>, PooledList<T>, PooledHashMap<K,V>, and PooledHashSet<T> in one registration — no need to add each formatter individually.

Immutable interop

using Prest;
using Prest.Immutable;

PooledArray<int> pooled = [1, 2, 3];
ImmutableArray<int> immutable = pooled.ToImmutableArray();

using var back = PooledArray.FromImmutableArray(immutable);

Choosing an algorithm

Algorithm Probe Best for Notes
Swiss (default) Triangular, 16-byte SIMD group scan Mixed read/write workloads with well-distributed keys Matches .NET 9's Dictionary. Uses tombstones on erase, which can degrade under heavy churn if keys hash densely — pair with a finalizer or pick RobinHood if you're churning sequential-int keys. SIMD path is fastest on x86 (single-instruction PMOVMSKB); ARM64 synthesizes ExtractMostSignificantBits in several instructions, so the Swiss-vs-alternatives gap narrows there.
RobinHood Linear with displacement tracking Integer-keyed hot paths; churn-heavy workloads; ARM64 Backward-shift on erase keeps displacements monotonic — no tombstones, no pathological churn. Fastest algorithm for integer-keyed lookup on ARM64 in our benchmarks (~37% faster than Dictionary, ~37% faster than Swiss).
Linear One slot at a time, scalar Tiny maps, debugging, a baseline for comparisons Simplest; no SIMD win but no SIMD cost on cold code paths either.
Chained Separate chaining with dense entry array Iteration-heavy workloads Entries are packed 0..Count-1, so foreach walks a dense array.

Quick rule of thumb:

  • Start with the default Swiss. It is within a few percent of Dictionary on lookup workloads and roughly 10–20% faster than Dictionary on Add (thanks to pooled arrays — no allocation).
  • If you do heavy add/remove churn on int/long keys, switch to RobinHood or keep Swiss and pair it with FibonacciFinalizer. Sequential or tightly-packed integer keys combined with identity hashing + tombstones is the one workload where Swiss can regress badly.
  • If your app is read-mostly and iterates maps often, Chained gives you the densest enumeration path.

Choosing a finalizer

Finalizer Mixing cost Use when…
NoOpHashFinalizer (default) Free (identity) Keys already have good GetHashCode distribution.
FibonacciFinalizer One multiply Sequential / strided integer keys (timestamps, counters, indices).
Lowbias32Finalizer Two multiplies + shifts You want stronger mixing without going full crypto — Wellons' lowest-avalanche-bias integer hash.
XmxFinalizer Xorshift–multiply–xorshift Adversarial or pathological input; strongest mixing.

Per-thread cache vs. ObjectPool

Both PooledHashMap<K,V>.Create() (core) and PooledHashMapPool<K,V>.Create() (Prest.ObjectPool) return a ready-to-use map; both auto-return on Dispose. The difference is the storage backing the pool:

  • Per-thread cache (core) — a [ThreadStatic] single-slot cache. Zero synchronisation. Ideal for synchronous request handlers and short-lived scopes. Do not keep a reference to the map after Dispose — another caller on this thread will rent it.
  • Prest.ObjectPoolMicrosoft.Extensions.ObjectPool.DefaultObjectPool (max 64 retained). Concurrent-safe; instances survive await boundaries. Use when the map must outlive a single synchronous scope.

You should never mix the two on the same instance — a map rented from PooledHashMapPool returns itself to that pool on Dispose, not to the per-thread cache.

Examples and benchmarks

examples/Prest.Examples/ — runnable mini-programs for each feature:

  • BasicMap.cs, BasicSet.cs — minimal CRUD.
  • Algorithms.cs — Swiss / RobinHood / Linear / Chained side-by-side.
  • CustomComparer.cs — wiring an IEqualityComparer<T>.
  • Finalizers.cs — Fibonacci + XMX finalizers on clustered keys.
  • Enumeration.csforeach, .Keys, .Values.

Run them:

dotnet run --project examples/Prest.Examples -c Release

benchmarks/Prest.Benchmarks/BenchmarkDotNet suites:

  • PooledHashMapBenchmarks.csPooledHashMap<int,int> vs Dictionary<int,int> vs Faster.Map.DenseMap<int,int> across lookup (hit + miss), ContainsKey, insert-from-empty, and steady-state churn at N ∈ {16, 256, 4096, 65536}.
  • PooledHashMapStringBenchmarks.cs — same comparisons on string-keyed maps.
  • AlgorithmComparisonBenchmarks.cs — the four algorithms head-to-head.
  • FinalizerComparisonBenchmarks.csNoOp / Fibonacci / Lowbias32 / XMX on Swiss and RobinHood with sequential-int keys.

Latest results are committed under benchmarks/artifacts/results/ (-report-github.md files render directly on GitHub).

dotnet run --project benchmarks/Prest.Benchmarks -c Release -- --filter '*'

Native AOT

All shipping packages set <IsAotCompatible>true</IsAotCompatible> on their .NET 8+ TFMs, so trim / AOT / single-file analyzers run in-build and fail on any public API that isn't AOT-safe. The core types, algorithms, object pools, PooledBufferWriter, and every individual converter/formatter are all analyzer-clean.

Two convenience APIs are flagged with [RequiresDynamicCode] + [RequiresUnreferencedCode] because they reach the closed converter/formatter type via Type.MakeGenericType:

  • Prest.Serializers.SystemTextJson.PooledTypesJsonConverterFactory
  • Prest.Serializers.VYaml.PooledTypeFormatterResolver

If your project uses Native AOT or trimming, skip these two and register the concrete converters directly — each is AOT-safe when closed over concrete generic arguments:

options.Converters.Add(new PooledArrayConverter<int>());
options.Converters.Add(new PooledListConverter<int>());
options.Converters.Add(new PooledHashSetConverter<int,
    SwissTableAlgorithm<int, int, EqualityDefaultHasher<int>, NoOpHashFinalizer>>());
options.Converters.Add(new PooledHashMapConverter<int, string,
    SwissTableAlgorithm<KeyValueSlot<int, string>, int, EqualityDefaultHasher<int>, NoOpHashFinalizer>>());

The same applies to VYaml: instead of adding PooledTypeFormatterResolver.Instance to your resolver chain, construct the concrete PooledArrayFormatter<T> / PooledListFormatter<T> / PooledHashMapFormatter<K,V,TAlgo> / PooledHashSetFormatter<T,TAlgo> you need at compile time and compose them into your own IYamlFormatterResolver.

Target frameworks

Package net10.0 net8.0 netstandard2.1 netstandard2.0
Prest
Prest.ObjectPool
Prest.SystemTextJson
Prest.SystemTextJson.ObjectPool
Prest.Serializers.SystemTextJson
Prest.Serializers.VYaml
Prest.Immutable

Prest.Serializers.SystemTextJson requires net8.0+ for IUtf8SpanFormattable. Prest.Immutable is net10.0-only (uses C# extension members).

License

MIT © kerem-acer. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.2 105 4/25/2026
0.1.1 97 4/23/2026
0.1.0 110 4/23/2026