Celerity.Collections
1.4.0
See the version list below for details.
dotnet add package Celerity.Collections --version 1.4.0
NuGet\Install-Package Celerity.Collections -Version 1.4.0
<PackageReference Include="Celerity.Collections" Version="1.4.0" />
<PackageVersion Include="Celerity.Collections" Version="1.4.0" />
<PackageReference Include="Celerity.Collections" />
paket add Celerity.Collections --version 1.4.0
#r "nuget: Celerity.Collections, 1.4.0"
#:package Celerity.Collections@1.4.0
#addin nuget:?package=Celerity.Collections&version=1.4.0
#tool nuget:?package=Celerity.Collections&version=1.4.0
Celerity
Celerity is a .NET library that provides specialized high-performance collections optimized for specific use cases. It includes data structures designed for better speed or memory efficiency compared to standard .NET collections. The package supports configurable load factors, multiple built-in hash functions, and allows users to define custom hash functions for fine-tuned performance.
Collections
CelerityDictionary<TKey, TValue, THasher>— generic dictionary with a struct hasher constraint.IntDictionary<TValue>/IntDictionary<TValue, THasher>—int-keyed specialization. Defaults toInt32WangNaiveHasher.LongDictionary<TValue>/LongDictionary<TValue, THasher>—long-keyed specialization. Defaults toInt64WangNaiveHasher.CeleritySet<T, THasher>— generic set counterpart toCelerityDictionary.IntSet/IntSet<THasher>—int-keyed set specialization.LongSet/LongSet<THasher>—long-keyed set specialization. Defaults toInt64WangNaiveHasher.
All dictionaries implement IReadOnlyDictionary<TKey, TValue?> and ship allocation-free struct enumerators, Keys / Values views, and an IEnumerable<KeyValuePair<TKey, TValue>> constructor. All collections handle default(TKey) (or zero for int / long keys, null for reference-type keys) out-of-band so it never collides with the empty-slot sentinel.
Quick start
Install from NuGet:
dotnet add package Celerity.Collections
IntDictionary — the int-keyed fast path
IntDictionary<TValue> defaults to Int32WangNaiveHasher, so most callers don't need to pick a hasher.
using Celerity.Collections;
var counts = new IntDictionary<int>();
counts[42] = 1;
counts[42]++; // indexer get/set
counts.TryAdd(7, 100); // returns false if key already present, no overwrite
counts.Add(8, 200); // throws ArgumentException if key already present
if (counts.TryGetValue(42, out var hits))
Console.WriteLine(hits); // 2
counts.Remove(7);
Console.WriteLine(counts.Count); // 2
// foreach is allocation-free — Enumerator is a struct.
foreach (var kvp in counts)
Console.WriteLine($"{kvp.Key} -> {kvp.Value}");
The zero key is a legitimate value, not the empty-slot sentinel — counts[0] = 99 round-trips correctly. LongDictionary<TValue> follows the exact same surface for long keys (defaulting to Int64WangNaiveHasher).
CelerityDictionary — generic keys with a struct hasher
For non-int/long keys, pick a hasher from Celerity.Hashing (or supply your own). DefaultHasher<T> falls back to EqualityComparer<T>.Default.GetHashCode() for arbitrary types.
using Celerity.Collections;
using Celerity.Hashing;
var byId = new CelerityDictionary<Guid, string, GuidHasher>();
byId[Guid.NewGuid()] = "alice";
var byName = new CelerityDictionary<string, int, StringFnV1AHasher>();
byName["bob"] = 1;
// DefaultHasher<T> works for any type but pays the EqualityComparer<T> dispatch.
var byKey = new CelerityDictionary<DateOnly, string, DefaultHasher<DateOnly>>();
byKey[DateOnly.FromDateTime(DateTime.UtcNow)] = "today";
The hasher is a struct and is supplied as a generic constraint, so the JIT devirtualizes and inlines the Hash() call on the probe path.
Sets
IntSet and CeleritySet<T, THasher> mirror the dictionary types for membership-only workloads.
using Celerity.Collections;
using Celerity.Hashing;
var seen = new IntSet();
seen.Add(1);
seen.Add(2);
Console.WriteLine(seen.Contains(1)); // true
seen.Remove(2);
var visitedIds = new CeleritySet<Guid, GuidHasher>();
visitedIds.TryAdd(Guid.NewGuid()); // returns true on first add, false on duplicate
Construct from an existing collection
The dictionaries accept any IEnumerable<KeyValuePair<TKey, TValue>>. When the source implements ICollection<T>, its Count is used to pre-size the backing storage so the bulk fill avoids resize work.
var bcl = new Dictionary<int, string> { [1] = "a", [2] = "b", [3] = "c" };
var fast = new IntDictionary<string>(bcl);
var fromKvps = new CelerityDictionary<string, int, StringFnV1AHasher>(
new[]
{
new KeyValuePair<string, int>("alice", 1),
new KeyValuePair<string, int>("bob", 2),
});
Duplicate keys (including duplicate default(TKey) / zero-key entries) throw ArgumentException, matching BCL Dictionary<,> semantics.
Custom hasher
Implement IHashProvider<T> as a struct to plug in your own hash function. See Custom hashing below for the contract and a worked example.
Choosing a collection
Celerity ships specialised types because each one buys a different tradeoff. Use the table below to pick the right one; if your workload doesn't appear here, the BCL collection is usually the right starting point.
| Your workload | Use | Why |
|---|---|---|
Dictionary keyed by int |
IntDictionary<TValue> |
Avoids generic boxing / EqualityComparer<int> dispatch; defaults to Int32WangNaiveHasher. |
Dictionary keyed by long |
LongDictionary<TValue> |
64-bit equivalent of IntDictionary; defaults to Int64WangNaiveHasher. |
Dictionary keyed by Guid, string, or any other type |
CelerityDictionary<TKey, TValue, THasher> |
Pick a struct hasher from Celerity.Hashing (e.g. GuidHasher, StringFnV1AHasher) so the JIT can inline Hash() on the probe path. |
Set of int values |
IntSet |
Same fast path as IntDictionary, membership only. |
Set of long values |
LongSet |
64-bit equivalent of IntSet; defaults to Int64WangNaiveHasher. |
| Set of any other type | CeleritySet<T, THasher> |
Same hasher choice as CelerityDictionary. |
| Need a stable iteration order, multi-threaded access, or a frozen / read-only post-build view | BCL Dictionary<,>, ConcurrentDictionary<,>, FrozenDictionary<,> |
Celerity is single-threaded, iteration order is unspecified, and FrozenCelerityDictionary is still on the 1.2.0 roadmap. |
Notes on picking a hasher once the collection is settled:
- For
int/longkeys, the convenience subclasses (IntDictionary<TValue>,IntSet,LongDictionary<TValue>,LongSet) already pick a sensible default — only override when you have evidence of clustered or adversarial keys, in which case escalate toInt32WangHasherthenInt32Murmur3Hasher(forintkeys) orInt64WangHasherthenInt64Murmur3Hasher(forlongkeys). The Wang full-finalizer tier is a cheaper middle option than Murmur3 while still mixing every input bit. - For
uintkeys,UInt32Hasheris the cheap XOR-fold default; escalate toUInt32WangHasher(the full Thomas-Wang finalizer) thenUInt32Murmur3Hasher(the Murmur3fmix32finalizer) when the fold produces clustering or you need strong avalanche on adversarial keys, mirroring theintfamily's two-stepInt32WangHasher→Int32Murmur3Hasherescalation. - For
stringkeys,StringFnV1AHasheris the fast default for ASCII-dominated workloads. Switch toStringMurmur3Hasherfor keys with significant non-ASCII content (it hashes the full UTF-16 character rather than just the low byte) or when key distribution is clustered or adversarial. - For arbitrary types,
DefaultHasher<T>(which delegates toEqualityComparer<T>.Default.GetHashCode()) is a safe fallback. It still benefits from the struct-hasher devirtualisation; the innerEqualityComparer<T>dispatch is the only unavoidable cost. Replace it with a hand-written struct hasher if profiling showsHashon the hot path. - The full hasher matrix lives in
docs/api/hashing.md.
A few cases where Celerity is not the right answer today:
- Concurrent reads/writes from multiple threads. Celerity collections are single-threaded; use
ConcurrentDictionary<,>or wrap a BCLDictionary<,>in your own lock. - You need
IDictionary<,>(mutable interface),LINQ-heavy code that relies onCount-via-extension on the boxed surface, or anything that depends on a specific iteration order. Celerity exposesIReadOnlyDictionary<,>only and does not guarantee iteration order across versions. - Build-once / read-many lookup tables for string keys. Today the BCL
FrozenDictionary<,>will outperformCelerityDictionaryon lookups; the Celerity equivalent (FrozenCelerityDictionary) is planned for 1.2.0 (#62).
Benchmarks
Up to 2.4× faster than Dictionary<int, int> on lookups, with zero allocations. The live dashboard tracks all five collections against their .NET BCL counterparts on every main push, with historical trends and per-PR regression comparisons. For high-precision local numbers, run dotnet run -c Release in src/Celerity.Benchmarks — hosted CI runners are noisier than your laptop and the dashboard reflects that.
Custom hashing
You can bring your own custom hash provider by implementing the IHashProvider<T> interface.
public interface IHashProvider<T>
{
int Hash(T key);
}
Hashers must be structs when used with Celerity collections (where THasher : struct, IHashProvider<T>) so the JIT can devirtualize and inline Hash(). The package ships built-in hashers for int, long, uint, ulong, Guid, and string, plus a DefaultHasher<T> fallback that delegates to EqualityComparer<T>.Default.GetHashCode(). See docs/api/hashing.md for the full list.
Not sure which hasher to pick for your key shape? HashQualityEvaluator.Evaluate<T, THasher>(keys) runs a representative key sample through a hasher and returns a HashQualityReport of collision count, bucket occupancy, max bucket load, chi-squared, and a normalized distribution score (1.0 = ideal uniform). It's a diagnostic tool — run it offline to compare candidate hashers before committing one. See the hash quality evaluation section.
Native AOT & trimming
Celerity is Native AOT and trimming compatible. The library carries no reflection, runtime code generation, or dynamic type loading — every collection is a generic over a struct hasher, and the only BCL primitives on the hot paths are MemoryMarshal, Unsafe, and EqualityComparer<T>.Default, all of which are AOT-safe. The assembly is marked <IsAotCompatible>true</IsAotCompatible>, so consuming it from a PublishAot app produces no trim or AOT warnings.
dotnet publish -r linux-x64 -c Release # in an app that references Celerity.Collections
Compatibility is enforced on every build: the trim and AOT Roslyn analyzers run as part of the library's compilation, and CI publishes a Native AOT smoke-test app that exercises every collection and hasher and runs the resulting native binary. See docs/aot.md for details.
API overview
The dictionaries (CelerityDictionary, IntDictionary, LongDictionary) expose a compact, allocation-conscious API that mirrors the parts of Dictionary<TKey, TValue> most users actually reach for: indexer get/set, ContainsKey, TryGetValue, Add, TryAdd, Remove (both the bool Remove(key) and bool Remove(key, out TValue?) overloads), Clear, Count, Keys, Values, and GetEnumerator(). They implement IReadOnlyDictionary<TKey, TValue?> and accept an IEnumerable<KeyValuePair<TKey, TValue>> source at construction.
The sets (CeleritySet, IntSet, LongSet) expose Add, TryAdd, Contains, Remove, Clear, Count, and a struct enumerator.
The zero / default(TKey) key (or element, for sets) is stored out-of-band so it never collides with the empty-slot sentinel used during probing. This includes null for reference-type keys.
For full API details — constructors, method signatures, parameters, exceptions, and usage examples — see the API reference docs.
Project docs
docs/— API reference (collections, hashing, utilities).ROADMAP.md— planned milestones and long-term vision.CHANGELOG.md— release notes.CONTRIBUTING.md— build, test, PR conventions.- GitHub Issues — open backlog and bug reports.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net8.0
- No dependencies.
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 |
|---|---|---|
| 1.4.1-beta.27 | 0 | 6/5/2026 |
| 1.4.1-beta.22 | 37 | 6/4/2026 |
| 1.4.1-beta.5 | 39 | 6/2/2026 |
| 1.4.1-beta.4 | 51 | 6/1/2026 |
| 1.4.1-beta.3 | 49 | 5/31/2026 |
| 1.4.0 | 89 | 5/31/2026 |
| 1.3.1-beta.2 | 62 | 5/28/2026 |
| 1.3.0 | 96 | 5/24/2026 |
| 1.2.2-beta.16 | 49 | 5/23/2026 |
| 1.2.2-beta.10 | 49 | 5/22/2026 |
| 1.2.2-beta.8 | 52 | 5/19/2026 |
| 1.2.2-beta.7 | 56 | 5/18/2026 |
| 1.2.2-beta.6 | 46 | 5/17/2026 |
| 1.2.1 | 87 | 5/17/2026 |
| 1.2.0 | 98 | 5/10/2026 |
| 1.1.2 | 90 | 5/1/2026 |
| 1.0.1 | 392 | 3/23/2025 |
| 0.0.0-beta.7 | 120 | 2/23/2025 |