You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,12 @@ All notable changes to Celerity are documented here. This project follows [Keep
6
6
7
7
### Added
8
8
9
+
- `RobinHoodDictionary<TKey, TValue, THasher>` in `Celerity.Collections` — a drop-in peer of `CelerityDictionary` that resolves collisions with **Robin Hood** open addressing instead of plain linear probing, delivering the 1.2.0 Robin Hood probing experiment ([#63](https://github.com/marius-bughiu/Celerity/issues/63)). Every occupied slot stores its probe sequence length (PSL — distance from the ideal slot) in a parallel `int[]`; on insert an incoming key that has travelled further than the resident displaces it ("robs from the rich") and the evicted entry is re-inserted further along, which bounds probe-length variance so worst-case lookups stay close to the average on clustered / adversarial key distributions. The stored PSL also lets a *negative* lookup terminate early — once the probe distance exceeds the resident slot's PSL the key cannot be present. Deletion is Robin Hood backward-shift (pull each follower back one slot, decrementing its PSL, until an empty or already-home slot). The public surface is byte-for-byte identical to `CelerityDictionary`: indexer get/set, `ContainsKey`, `ContainsValue`, `TryGetValue`, `Add`, `TryAdd`, `Remove` (both `bool Remove(key)` and `bool Remove(key, out TValue?)`), `Clear`, `Count`, allocation-free struct `Keys` / `Values` views and `Enumerator` (with `_version` mutation detection), `IReadOnlyDictionary<TKey, TValue?>`, and both constructors (`(capacity, loadFactor)` and `(IEnumerable<KeyValuePair<TKey, TValue>>, capacity, loadFactor)` with the same power-of-two sizing, load-factor-headroom bulk sizing, validation, and out-of-band `default(TKey)` handling). The insert path computes the key hash once and threads it through both the existence probe and the displacement insertion, so a new-key `Add` / `TryAdd` / indexer-set costs exactly one `Hash()` call (displaced residents are never re-hashed). The cost versus `CelerityDictionary` is the per-slot PSL `int` (more allocation) and a little extra insert work for the swaps; on uniform keys with a good hasher it is a wash or a slight loss, so it is documented as the pick for the clustered / adversarial case rather than a default. AOT-safe; hot-path lookup is allocation-free.
10
+
- `RobinHoodDictionaryTests`, `RobinHoodDictionaryCollisionTests`, and `RobinHoodDictionaryEnumerationTests` — dedicated suites mirroring the `CelerityDictionary*` files. The functional tests cover insert/retrieve/overwrite, `ContainsKey`, `Remove`, resize, the out-of-band `0` / `Guid.Empty` default key surviving resize, `TryGetValue` hit/miss, `Clear` (including the already-empty no-op), and `Add` / `TryAdd` duplicate semantics; the collision suite adds full-collision insert/overwrite/remove/resize chains, the Robin Hood **displacement** path (a longer-travelled key robbing a richer resident while every key stays reachable), early-terminating negative lookups on clustered keys, `null`-string-key round-trips, and a **differential fuzz** (`RandomizedOps_ShouldMatchBclDictionary`, 5000 random Set/Remove/Lookup ops over a deliberately tiny key universe across four seeds) reconciled against a `Dictionary<int, int>` oracle to stress the swap / backward-shift machinery; the enumeration suite mirrors the full `Keys` / `Values` / `Enumerator` surface including default-key yield, mid-enumeration mutation detection, and the boxed generic / non-generic `IEnumerable` paths. A `RobinHoodDictionary` arm was also added to the differential testing layer: a CsCheck model property test (`CollectionModelPropertyTests.RobinHoodDictionary_ShouldMatch_BclDictionary`) and a `Celerity.Fuzz` target (`RobinHoodDictionary`, registered in `Differential.All`), both reconciled against a `Dictionary<int, int>` oracle.
11
+
- Cross-collection shared tests extended to cover `RobinHoodDictionary` alongside `CelerityDictionary` in all thirteen shared files: `AddAndTryAddTests`, `ConstructorValidationTests`, `IEnumerableConstructorTests`, `IEnumerableConstructorNullPriorityTests`, `ReadOnlyDictionaryInterfaceTests`, `RemoveOutValueTests`, `ContainsValueTests`, `LoadFactorBoundaryTests`, `IndexerReturnTypeTests`, `IndexerOverwriteResizeTests` (overwrite at threshold does not resize), `TryAddDuplicateResizeTests` (duplicate `TryAdd` at threshold keeps an active enumerator valid; a new-key `TryAdd` invalidates it), `TryAddProbeCountTests` (a new-key / duplicate `TryAdd` does exactly one `Hash()` call — the single-hash insert path), and `BulkConstructorNoResizeTests` (a bulk construct from a known-`Count` source costs exactly N hashes with no resize).
12
+
-`RobinHoodDictionaryBenchmark` in `Celerity.Benchmarks`, mirroring `CelerityDictionaryBenchmark` (Insert / Lookup / Remove against a `Dictionary<int, int>` baseline at 1k / 100k items, with the per-`Remove``[IterationSetup]` pattern), and registered in `Program.cs`'s core `--ci``benchmarkTypes` so the joined CI report and the gh-pages dashboard include it.
13
+
- Benchmark dashboard wiring for `RobinHoodDictionary`: a "What ships in the box" ship card in `web/index.html`, and a `COLLECTIONS` entry (key / title / vs / ops) in both `web/dev/bench/index.html` and `web/dev/bench/detail.html` so the auto-published `data.js` is surfaced rather than ignored.
14
+
- Documentation for `RobinHoodDictionary`: a full section in `docs/api/collections.md` (what Robin Hood probing does, when to pick it over `CelerityDictionary`, constructors, default-key handling, runnable usage example) and README updates — the Collections list, the "Choosing a collection" decision table, and a "Quick start" subsection.
9
15
- `SmallDictionary<TKey, TValue>` in `Celerity.Collections` — a dictionary tuned for the very-small (`n <= ~16`) case, where a linear scan over a flat backing array beats a probe-based hash table (the shape compilers, IL emitters, AST attribute bags, and per-request maps hit constantly). The 1.2.0 small-collection ([#61](https://github.com/marius-bughiu/Celerity/issues/61)). It stores entries in insertion-dense parallel `TKey?[]` / `TValue?[]` arrays and answers every query with a linear scan using `EqualityComparer<TKey>.Default`, so there is **no hasher** (no `THasher` type parameter): nothing is hashed, which means there is no empty-slot sentinel and therefore **no out-of-band default-key slot** — a `0` / `null` / `Guid.Empty` key is stored inline like any other, a deliberate simplification over the hash-table dictionaries. The trade-off is that lookups / `Add` / `TryAdd` / `ContainsKey` / `Remove` are `O(n)` rather than `O(1)`, so the type is for small key sets and is documented as degrading for large ones (it does not auto-promote to a hash table; it grows its arrays and keeps scanning). `Remove` swaps the last entry into the vacated slot (an `O(1)` move once the key is found), so enumeration order is unspecified; a pure indexer overwrite never grows the arrays. Public API mirrors the other Celerity dictionaries: indexer get/set (get returns the non-nullable `TValue` and throws `KeyNotFoundException` on miss), `ContainsKey`, `ContainsValue`, `TryGetValue`, `Add`, `TryAdd`, `Remove` (both `bool Remove(key)` and `bool Remove(key, out TValue?)`), `Clear`, `Count`, allocation-free struct `Keys` / `Values` views and `Enumerator` (with `_version` mutation detection), `IReadOnlyDictionary<TKey, TValue?>`, and an `IEnumerable<KeyValuePair<TKey, TValue>>` constructor (duplicate keys throw `ArgumentException`; a `null` source throws `ArgumentNullException`, checked before the capacity validation). The constructor takes a `capacity` (used verbatim, not rounded to a power of two, since there is no probe mask; `0` defers allocation) and — unlike the hash-table dictionaries — has **no `loadFactor`** parameter. AOT-safe (no reflection); hot-path lookup is allocation-free.
10
16
-`SmallDictionaryTests` and `SmallDictionaryEnumerationTests` — dedicated suites mirroring `IntDictionaryTests` / `IntDictionaryEnumerationTests`, adapted for a hasher-less type: indexer insert/retrieve/overwrite (and overwrite-at-capacity not growing), `Remove` from first / middle / last slot via the swap-with-last path, grow-on-capacity-exceeded, the inline `0` / `null` default key exercised as an ordinary entry, `TryGetValue` hit/miss, `Clear` (including the already-empty no-op), remove-then-reinsert fidelity, zero-capacity deferred allocation, and the full enumeration surface (yield-once, reflect removal/clear, survive growth, mid-enumeration mutation/remove/clear detection on `MoveNext` and `Reset`, `Keys` / `Values` views and counts, the boxed generic and non-generic `IEnumerable` paths, and `Reset` reuse). A dedicated `*CollisionTests` file is intentionally **not** added: a linear-scan dictionary has no hashing and therefore no collisions to test.
11
17
- A `SmallDictionary` parity arm added to the differential testing layer: a CsCheck model property test (`CollectionModelPropertyTests.SmallDictionary_ShouldMatch_BclDictionary`, against a `Dictionary<int, int>` oracle over a random Set/Remove/TryAdd/Clear op stream) and a `Celerity.Fuzz` target (`SmallDictionary`, registered in `Differential.All`) that fuzzes the same op stream against the BCL oracle and cross-checks count, per-key lookups, and duplicate-free enumeration.
Copy file name to clipboardExpand all lines: README.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,7 @@ Celerity is a .NET library that provides specialized high-performance collection
6
6
## Collections
7
7
8
8
-`CelerityDictionary<TKey, TValue, THasher>` — generic dictionary with a struct hasher constraint.
9
+
-`RobinHoodDictionary<TKey, TValue, THasher>` — `CelerityDictionary`'s peer using Robin Hood open addressing: bounds probe-length variance so worst-case lookups stay close to average on clustered / adversarial keys (at the cost of a per-slot probe-distance `int`).
9
10
-`FrozenCelerityDictionary<TValue>` / `FrozenCelerityDictionary<TValue, THasher>` — build-once, read-many `string`-keyed dictionary that searches for a perfect (collision-free) hash so lookups are single-probe. Defaults to `StringFnV1AHasher`.
10
11
-`CelerityMultiMap<TKey, TValue, THasher>` — one-to-many map: each key groups multiple values (`Add` appends rather than overwrites). Implements `ILookup<TKey, TValue?>`.
11
12
-`SmallDictionary<TKey, TValue>` — flat-array, linear-scan dictionary tuned for the very-small (`n <= ~16`) case. No hasher: it never hashes, so a `0` / `null` / default key is stored inline rather than out-of-band.
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.
74
75
76
+
### `RobinHoodDictionary` — bounded probe variance for clustered keys
77
+
78
+
When keys bunch up (weak or identity hashers, attacker-influenced keys, naturally clustered IDs), linear probing grows long runs and worst-case lookups degrade. `RobinHoodDictionary` is a drop-in peer of `CelerityDictionary` — same API, same hashers — that uses Robin Hood open addressing to keep probe-length variance low, so tail-latency lookups stay close to the average. It also stops a *negative* lookup early using its probe-distance invariant.
The trade-off is a per-slot probe-distance `int` of bookkeeping (so it allocates more than `CelerityDictionary`); on uniformly distributed keys with a good hasher, `CelerityDictionary` matches or beats it, so prefer Robin Hood specifically for the clustered / adversarial case.
When a `string`-keyed table is built once and then read many times (route tables, config maps, interned vocabularies), `FrozenCelerityDictionary<TValue>` searches at construction for a perfect (collision-free) hash so each lookup is a single hash, a single array index, and a single equality check.
@@ -189,6 +207,7 @@ Celerity ships specialised types because each one buys a different tradeoff. Use
189
207
| Dictionary keyed by `int`|`IntDictionary<TValue>`| Avoids generic boxing / `EqualityComparer<int>` dispatch; defaults to `Int32WangNaiveHasher`. |
190
208
| Dictionary keyed by `long`|`LongDictionary<TValue>`| 64-bit equivalent of `IntDictionary`; defaults to `Int64WangNaiveHasher`. |
191
209
| 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. |
210
+
| Dictionary with **clustered / adversarial** keys where worst-case lookup latency matters |`RobinHoodDictionary<TKey, TValue, THasher>`| Same API as `CelerityDictionary`, but Robin Hood probing bounds probe-length variance so tail-latency lookups don't degrade on bunched keys. Costs a per-slot probe-distance `int`; for uniform keys with a good hasher, prefer `CelerityDictionary`. |
192
211
| Build-once, read-many lookup table keyed by `string`|`FrozenCelerityDictionary<TValue>`| Immutable; searches for a perfect (collision-free) hash at build time so lookups are single-probe. Tune the hasher via the `<TValue, THasher>` overload. |
193
212
| One key maps to **many** values (one-to-many) |`CelerityMultiMap<TKey, TValue, THasher>`|`Add` appends to a per-key value group instead of overwriting; implements `ILookup<,>`. Pick the struct hasher for your key type, as with `CelerityDictionary`. |
194
213
| Tiny dictionary (`n <= ~16`) that stays small |`SmallDictionary<TKey, TValue>`| Flat-array linear scan beats hashing at small `n` — no hash to compute, great cache locality, no hasher to pick. Degrades to `O(n)` for large key sets, so only when instances stay small. |
Copy file name to clipboardExpand all lines: ROADMAP.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,7 +77,7 @@ Focus on raw performance and specialized collection types that serve more advanc
77
77
78
78
### Performance
79
79
80
-
- Robin Hood hashing experiment as alternative to linear probing. Tracked in [#63](https://github.com/marius-bughiu/Celerity/issues/63).
80
+
- Robin Hood hashing experiment as alternative to linear probing. Status: `done` — shipped as a new collection type, `RobinHoodDictionary<TKey, TValue, THasher>`, a drop-in peer of `CelerityDictionary` that uses Robin Hood open addressing (per-slot probe sequence length, displace-the-richer-resident inserts, backward-shift-with-PSL-decrement deletes) to bound probe-length variance and keep worst-case lookups close to the average on clustered / adversarial keys; negative lookups terminate early via the PSL invariant. The default is unchanged — this is an additional opt-in type for the clustered case, not a replacement (the per-slot PSL `int` and extra insert work make it a wash or a slight loss on uniform keys). Tracked in [#63](https://github.com/marius-bughiu/Celerity/issues/63).
81
81
- Performance optimizations across existing collections.
82
82
- Native AOT support and trimming compatibility. Status: `done` — the library is marked `<IsAotCompatible>true</IsAotCompatible>` (trim + AOT analyzers run on every build), and a Native AOT publish smoke test runs the full collection / hasher surface as a native binary in CI. See [`docs/aot.md`](docs/aot.md). An AOT-vs-JIT benchmark comparison remains a follow-up. Tracked in [#32](https://github.com/marius-bughiu/Celerity/issues/32).
A drop-in peer of `CelerityDictionary` that resolves collisions with **Robin Hood** open addressing instead of plain linear probing. The public surface — constructors, indexer, `ContainsKey` / `ContainsValue` / `TryGetValue` / `Add` / `TryAdd` / `Remove` / `Clear`, the struct `Enumerator` / `KeyCollection` / `ValueCollection`, and `IReadOnlyDictionary<TKey, TValue?>` — is identical to `CelerityDictionary`. Only the probing strategy differs.
0 commit comments