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
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,13 @@ All notable changes to Celerity are documented here. This project follows [Keep
6
6
7
7
### Added
8
8
9
+
- `FrozenCeleritySet` / `FrozenCeleritySet<THasher>` in `Celerity.Collections` — a build-once, read-many set of `string` elements, the **set counterpart of `FrozenCelerityDictionary`** and the second half of the 1.2.0 frozen-collections work ([#22](https://github.com/marius-bughiu/Celerity/issues/22)). Like the frozen dictionary it is immutable (constructed once from an `IEnumerable<string>`, no mutating members) and at construction searches a small parameter space — candidate power-of-two table sizes × a per-build mixing seed — for a **perfect** (collision-free) placement of the elements; when found (`IsPerfectlyHashed == true`) a `Contains` is a single hash, a single array index, and a single equality check, with no probing. When two distinct elements collide on the chosen hasher's raw 32-bit code (e.g. `"A"` / `"Ł"` under the low-byte `StringFnV1AHasher`), a perfect placement is impossible, so the build falls back to an open-addressed linear-probing table (`IsPerfectlyHashed == false`); membership tests stay correct (the equality check disambiguates), just not single-probe. The one contract difference from the frozen *dictionary* is intentional and idiomatic for a set: **duplicate elements are silently deduplicated** (matching BCL `FrozenSet<T>` and the mutable `CeleritySet`) rather than throwing. Read API: `Contains`, `Count`, `IsPerfectlyHashed`, an allocation-free struct `Enumerator` (the out-of-band `null` element, if present, is yielded first), and the full `IReadOnlySet<string>` surface (`SetEquals`, `IsSubsetOf`, `IsProperSubsetOf`, `IsSupersetOf`, `IsProperSupersetOf`, `Overlaps` — the superset/overlap shapes stream `other` against the `O(1)` membership test, the subset/equality shapes materialize `other`'s distinct elements once into an ordinal set, and each throws `ArgumentNullException` on a `null` `other`). The `null` element is stored out-of-band (the hasher is never invoked with `null`) and the empty string is an ordinary element. The convenience `FrozenCeleritySet` defaults to `StringFnV1AHasher`; the `<THasher>` overload accepts any string hasher. AOT-safe (no reflection); hot-path membership is allocation-free.
10
+
- `FrozenCeleritySetTests` — a dedicated suite mirroring `FrozenCelerityDictionaryTests` for the set (construction & round-trip, present/absent `Contains`, empty-string-as-regular-element, out-of-band `null`-element round-trip and absence, `null`-source rejection, **duplicate / duplicate-`null` silent dedupe** (the set-vs-dictionary contract difference), empty source, the perfect fast path asserted via `IsPerfectlyHashed` on a strong hasher, the **fallback correctness** case where base-hash-colliding `"A"` / `"Ł"` under `StringFnV1AHasher` force linear probing yet stay distinct, absent-elements-hashing-into-occupied-slots miss, a 1000-element build-and-round-trip sweep with 1000 absent-element misses, enumeration including the `null` element, the `IReadOnlySet` surface, **the full set-algebra matrix** (`SetEquals` / `IsSubsetOf` / `IsProperSubsetOf` / `IsSupersetOf` / `IsProperSupersetOf` / `Overlaps`, including duplicates-in-`other`, the `null` element, and `null`-`other` throwing), the custom-hasher overload, the default convenience type, and the non-generic `IEnumerable` / `Reset` paths).
11
+
- A `FrozenCeleritySet` parity arm added to the differential testing layer: a CsCheck model property test (`CollectionModelPropertyTests.FrozenCeleritySet_ShouldMatch_BclHashSet`, building from a duplicate-rich source and reconciling distinct membership, absent-element misses, full-enumeration round-trip, and set-algebra against a `HashSet<string>` oracle) and a `Celerity.Fuzz` target (`FrozenCeleritySet`, registered in `Differential.All`) that fuzzes a duplicate-rich source against the BCL oracle and cross-checks count, per-element `Contains`, duplicate-free enumeration, and set-algebra.
12
+
- Cross-collection shared tests extended to cover `FrozenCeleritySet` in `SetIEnumerableConstructorTests` (the mirror of the dictionary `IEnumerableConstructorTests`): null-source `ArgumentNullException`, empty source, array / non-collection-enumerable copy, duplicate and duplicate-`null` dedupe, the out-of-band `null` element, large-source fidelity, source independence, the `<THasher>` open-generic overload, and a frozen-set-from-frozen-set round-trip. The hash-table-mutation shared files genuinely do not apply and are intentionally not extended: `SetConstructorValidationTests` (no `capacity` / `loadFactor` — the perfect-hash build sizes the table itself), `TryAddDuplicateResizeTests` / `TryAddProbeCountTests` (immutable, no `TryAdd`), and the dictionary-only shared files.
13
+
-`FrozenCeleritySetBenchmark` in `Celerity.Benchmarks`, mirroring `FrozenCelerityDictionaryBenchmark` — a `Build` + `Contains` comparison of `FrozenCeleritySet` against the BCL `System.Collections.Frozen.FrozenSet<string>` baseline (the true build-once counterpart; a mutable `HashSet<>` does no build-time hashing optimization, so it is not a like-for-like baseline for a frozen, perfect-hashed set), over the same `[Params(1000, 100_000)]` identifier-shaped distinct element sets. Registered in `Celerity.Benchmarks/Program.cs`'s core `--ci``CoreBenchmarks` array so it joins the `RunAllJoined` CI report and the gh-pages dashboard.
14
+
- Benchmark dashboard wiring for `FrozenCeleritySet`: a "What ships in the box" ship card in `web/index.html`, and a `COLLECTIONS` entry (key / title / vs / ops `['Build', 'Contains']`) in both `web/dev/bench/index.html` and `web/dev/bench/detail.html`. `FrozenSet` was added to each dashboard's `BCL_TYPES` set so the parser treats the benchmark's `FrozenSet_*` methods as the baseline series (the comparison is Celerity-frozen vs BCL-frozen rather than vs the mutable `HashSet<>`).
15
+
- Documentation for `FrozenCeleritySet`: a full section in `docs/api/collections.md` (both the convenience and `<THasher>` overloads: the perfect-hash design and the set-vs-dictionary dedupe contract, constructor, `Count` / `IsPerfectlyHashed`, `Contains`, the `IReadOnlySet` set-algebra surface, null-element handling, the perfect fast path vs the linear-probing fallback, and runnable examples), and README updates — the Collections list, the "Choosing a collection" decision table, and a new "build-once string membership" Quick start subsection. The `Celerity.AotSmokeTest` now constructs `FrozenCeleritySet` (default, with the out-of-band `null` element and the `IReadOnlySet` surface), `FrozenCeleritySet<StringMurmur3Hasher>`, and the base-hash-collision fallback (`StringFnV1AHasher` over `"A"` / `"Ł"`) so the Native AOT publish job compiles the new generic instantiations and exercises both lookup paths.
9
16
- `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
17
- `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
18
- 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).
Copy file name to clipboardExpand all lines: README.md
+29Lines changed: 29 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ Celerity is a .NET library that provides specialized high-performance collection
13
13
-`IntDictionary<TValue>` / `IntDictionary<TValue, THasher>` — `int`-keyed specialization. Defaults to `Int32WangNaiveHasher`.
14
14
-`LongDictionary<TValue>` / `LongDictionary<TValue, THasher>` — `long`-keyed specialization. Defaults to `Int64WangNaiveHasher`.
15
15
-`CeleritySet<T, THasher>` — generic set counterpart to `CelerityDictionary`.
16
+
-`FrozenCeleritySet` / `FrozenCeleritySet<THasher>` — build-once, read-many `string` set that searches for a perfect (collision-free) hash so membership tests are single-probe. The set counterpart of `FrozenCelerityDictionary`; implements `IReadOnlySet<string>`. Defaults to `StringFnV1AHasher`.
16
17
-`IntSet` / `IntSet<THasher>` — `int`-keyed set specialization.
17
18
-`LongSet` / `LongSet<THasher>` — `long`-keyed set specialization. Defaults to `Int64WangNaiveHasher`.
18
19
@@ -176,6 +177,33 @@ var visitedIds = new CeleritySet<Guid, GuidHasher>();
176
177
visitedIds.TryAdd(Guid.NewGuid()); // returns true on first add, false on duplicate
It is immutable (no `Add` / `Remove`) and implements `IReadOnlySet<string>` (so `SetEquals`,
201
+
`IsSubsetOf`, `Overlaps`, … are all available). Duplicate elements are silently deduplicated, as
202
+
a set should. The default uses `StringFnV1AHasher`; supply a full-width or strong hasher via
203
+
`FrozenCeleritySet<THasher>` (e.g. `StringFnV1AFullHasher` for non-ASCII elements) when you want
204
+
the single-probe fast path for elements the default would collide. Membership tests stay correct
205
+
regardless — colliding elements fall back to a short probe.
206
+
179
207
### Construct from an existing collection
180
208
181
209
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.
@@ -214,6 +242,7 @@ Celerity ships specialised types because each one buys a different tradeoff. Use
214
242
| Set of `int` values |`IntSet`| Same fast path as `IntDictionary`, membership only. |
215
243
| Set of `long` values |`LongSet`| 64-bit equivalent of `IntSet`; defaults to `Int64WangNaiveHasher`. |
216
244
| Set of any other type |`CeleritySet<T, THasher>`| Same hasher choice as `CelerityDictionary`. |
245
+
| Build-once, read-many membership set keyed by `string`|`FrozenCeleritySet`| Immutable; searches for a perfect (collision-free) hash at build time so `Contains` is single-probe. The set counterpart of `FrozenCelerityDictionary`; implements `IReadOnlySet<string>`. Tune the hasher via the `<THasher>` overload. |
217
246
| Need a stable iteration order or multi-threaded access | BCL `Dictionary<,>`, `ConcurrentDictionary<,>`| Celerity is single-threaded and iteration order is unspecified. |
218
247
219
248
Notes on picking a hasher once the collection is settled:
Copy file name to clipboardExpand all lines: ROADMAP.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,6 +72,7 @@ Focus on raw performance and specialized collection types that serve more advanc
72
72
### Collections
73
73
74
74
-`FrozenCelerityDictionary` — build-once, read-many variant with perfect hashing for string keys, comparable in spirit to `System.Collections.Frozen` but tunable via `IHashProvider<T>`. Status: `done` — `FrozenCelerityDictionary<TValue>` / `<TValue, THasher>` search for a collision-free single-probe layout at construction and fall back to linear probing when the chosen hasher collides two keys' raw codes, so lookups are always correct. Tracked in [#62](https://github.com/marius-bughiu/Celerity/issues/62).
75
+
- Frozen collections family — the set counterpart `FrozenCeleritySet` / `FrozenCeleritySet<THasher>` completes the build-once read-many family (`FrozenCelerityDictionary` → `FrozenCeleritySet`), sharing the same perfect-hash-with-linear-probing-fallback build and implementing `IReadOnlySet<string>`. Status: `done`. Tracked in [#22](https://github.com/marius-bughiu/Celerity/issues/22).
75
76
-`CelerityMultiMap<TKey, TValue, THasher>` — multi-value dictionary. Status: `done` — a one-to-many map that reuses `CelerityDictionary`'s open-addressed key table and stores a `List<TValue?>` value group per key; `Add` appends rather than overwrites, `Remove(key, value)` / `RemoveAll(key)` are the two removal shapes, the indexer returns an empty group for an absent key, and the type implements `ILookup<TKey, TValue?>`. Tracked in [#18](https://github.com/marius-bughiu/Celerity/issues/18).
76
77
-`SmallDictionary<TKey, TValue>` — flat-array implementation optimized for `n <= ~16`. Status: `done` — `SmallDictionary<TKey, TValue>` linear-scans insertion-dense parallel arrays with `EqualityComparer<TKey>.Default` (no hasher, so the default key is stored inline rather than out-of-band), trading `O(1)` for `O(n)` to win at small `n`; it implements `IReadOnlyDictionary<TKey, TValue?>` with the full dictionary surface. Tracked in [#61](https://github.com/marius-bughiu/Celerity/issues/61).
0 commit comments