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
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,14 @@ All notable changes to Celerity are documented here. This project follows [Keep
6
6
7
7
### Added
8
8
9
+
- `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
+
-`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
+
- 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.
12
+
- Cross-collection shared tests extended to cover `SmallDictionary`: `AddAndTryAddTests` (Add/TryAdd new/duplicate/zero-key, duplicate leaves value unchanged), `ConstructorValidationTests` (negative-capacity throw and zero-capacity accept — the loadFactor rows genuinely do not apply, as `SmallDictionary` has no load factor), `ContainsValueTests` (empty-map false, regular-slot hit, default/zero value, post-`Remove` the swapped-out value does not linger, `null` value), `IEnumerableConstructorTests` (null-source `ArgumentNullException` with `paramName` `"source"`, duplicate-key `ArgumentException`, array / non-collection-enumerable copy, large-source fidelity), `IEnumerableConstructorNullPriorityTests` (null source beats the only other ctor validation it has — a negative `capacity`), `IndexerReturnTypeTests` (the primary indexer's declared return type is the non-nullable `TValue`), `ReadOnlyDictionaryInterfaceTests` (assignability, interface indexer / `TryGetValue` / `Keys` / `Values` / generic + non-generic enumeration / LINQ `Count`, and the polymorphic `IReadOnlyDictionary<int,int>` consumer), `RemoveOutValueTests` (captured-value path, missing-key / empty-map false, default-value capture, middle-slot removal keeping the rest, remove-then-reinsert, version bump invalidating an active enumerator), and `TryAddDuplicateResizeTests` (a duplicate `TryAdd` at exactly full capacity is a no-op that keeps an active enumerator valid; a new-key `TryAdd` that grows the arrays invalidates it). The hash-specific shared files do not apply and are intentionally not extended: `LoadFactorBoundaryTests` (no load factor), `TryAddProbeCountTests` and `IndexerOverwriteResizeTests` (both count `IHashProvider.Hash` calls — there is no hasher), and the `Set*` files (sets only).
13
+
-`SmallDictionaryBenchmark` in `Celerity.Benchmarks` — an `Insert` / `Lookup` / `Remove` comparison of `SmallDictionary<int, int>` against the BCL `Dictionary<int, int>` baseline. Unlike the hash-table benchmarks it uses **small**`[Params(8, 64)]` item counts rather than `1000 / 100_000`: `SmallDictionary` is a small-`n` collection whose `O(n)` scan is meant to win at low `n` and lose as `n` grows (and an `O(n²)` insert sweep at 100k would be both meaningless and far too slow for CI), so 8 (a clear win) and 64 (into the crossover region) show both sides honestly. Same per-`Remove``[IterationSetup]` rebuild pattern as the other collection benchmarks. Registered in `Celerity.Benchmarks/Program.cs`'s `CoreBenchmarks` array so it joins the `RunAllJoined` CI report consumed by `github-action-benchmark` and published to the gh-pages benchmark history on every push to `main`.
14
+
- Dashboard wiring for `SmallDictionary`: a "What ships in the box" ship card in `web/index.html`, and a `COLLECTIONS` entry in both `web/dev/bench/index.html` (key / title / vs `Dictionary<int, int>` / ops `['Insert', 'Lookup', 'Remove']`) and `web/dev/bench/detail.html` (key / title / vs). Because `SmallDictionary` is benchmarked at `8 / 64` items rather than the `1000 / 100_000` the hash tables use, both dashboards were generalized to support **per-collection item counts**: each `COLLECTIONS` entry may now carry an optional `items` array (defaulting to `[1000, 100000]` when absent, preserving every existing collection's behaviour), and the chart cells, headline detail strings, detail-page item-count toggle buttons, and URL/param validation read from it. The baseline series parses as the existing `Dictionary``BCL_TYPES` entry (the benchmark's baseline methods are named `Dictionary_*`), so no `BCL_TYPES` change is needed.
15
+
-`docs/api/collections.md` and `README.md` — a full `SmallDictionary` section in the API reference (the no-hasher / flat-array design and its trade-offs, constructors with the verbatim `capacity` and the absent `loadFactor`, `Count`, the indexer, every method, the inline default-key handling, and a runnable per-scope-symbol-table example), the README Collections list and "Choosing a collection" decision table updated to ship it (with the small-`n`-only guidance), a new "the tiny-map fast path" Quick start subsection, and the "all collections handle `default(TKey)` out-of-band" note corrected to carve out `SmallDictionary`'s inline default key. The `Celerity.AotSmokeTest` now constructs `SmallDictionary<int, int>` (indexer / Add / TryAdd / Remove / inline zero key / enumeration) and `SmallDictionary<string, int>` (the `IEnumerable` ctor and inline `null` key) so the Native AOT publish job compiles the new generic instantiations.
16
+
9
17
- Expanded the benchmark project (`src/Celerity.Benchmarks`) with an **extended, on-demand suite** that goes beyond the single random-key comparison ([#26](https://github.com/marius-bughiu/Celerity/issues/26), [#60](https://github.com/marius-bughiu/Celerity/issues/60)). New `KeyDistributions` helper generates **uniform / sequential / clustered / adversarial** integer and long key sets, and eight new benchmark classes use them: `DistributionBenchmark` (insert/lookup across distributions), `AdversarialHasherBenchmark` (shows `Int32WangNaiveHasher` degrading to O(n) on engineered collisions while `Int32Murmur3Hasher` stays O(1)), `LargeDatasetBenchmark` (1M/5M items), `MemoryAllocationBenchmark` (grow-vs-presized allocations with the full `MemoryDiagnoser` columns), `ConcurrentAccessBenchmark` (read scaling at 1/4/8 threads vs `ConcurrentDictionary<,>`), `CacheLocalityBenchmark` (in-order vs shuffled probing), `LibraryComparisonBenchmark` (vs the BCL `FrozenDictionary<,>`), and `RealWorldWorkloadBenchmark` (mixed ~80/12/8 read/write/remove stream with a hot key set). These run on demand (e.g. `dotnet run -c Release -- --filter "*Distribution*"`) and are **kept out of the per-PR CI regression run** so the same-runner A/B comparison stays fast and low-variance; `Program.cs` now splits a CI-tracked `CoreBenchmarks` set from the local-only `ExtendedBenchmarks` set. In CI they additionally run **weekly** (and on demand) via a new `benchmarks-extended.yml` workflow (`--ci-extended`), which publishes to a **separate `dev/bench-extended` dashboard** on gh-pages — linked as *Extended* in the site nav and kept apart from the per-commit core trend. Documented in [`docs/performance.md`](docs/performance.md#extended-benchmark-suite).
10
18
- `CelerityMultiMap<TKey, TValue, THasher>` in `Celerity.Collections` — a one-to-many map (multi-map / multi-dictionary): each key maps to an ordered *group* of values rather than a single value. The 1.2.0 one-to-many collection ([#18](https://github.com/marius-bughiu/Celerity/issues/18)). It reuses `CelerityDictionary`'s open-addressed, linear-probing key table and the same `where THasher : struct, IHashProvider<TKey>` constraint (so the JIT devirtualizes and inlines the key hash), storing a `List<TValue?>` value group alongside each key slot. `Add(key, value)` **always appends** — adding the same key twice groups the values, and adding the same value twice under one key keeps both copies. Public API: `Add`, `AddRange`, `Remove(key, value)` (removes one occurrence; removes the key when its group empties), `RemoveAll(key)` (removes the key and all its values), `Clear`, `ContainsKey`, `Contains(key, value)`, `ContainsValue(value)`, `CountValues(key)`, `TryGetValues(key, out ValueGroup)`, the `ValueGroup this[key]` indexer (returns an **empty group** for an absent key, matching `ILookup` semantics, rather than throwing), `Count` (distinct keys) and `ValueCount` (total values), an allocation-free struct `Enumerator` yielding one `Grouping` (`IGrouping<TKey, TValue?>`) per key, and a `Keys` view. Reads are allocation-free (the indexer / `TryGetValues` hand back a lightweight `ValueGroup` struct over the live backing list); the write path allocates one backing list per distinct key, inherent to storing a group per key. Implements `ILookup<TKey, TValue?>`, so it flows through LINQ. `default(TKey)` (`null` / `0` / `Guid.Empty`) is stored out-of-band — the hasher is never invoked with it, so it never collides with the empty-slot sentinel — and behaves as an ordinary key (yielded first in enumeration). The `IEnumerable<KeyValuePair<TKey, TValue>>` constructor groups duplicate keys in source order (duplicate keys are not an error, unlike the dictionaries); a `null` source throws `ArgumentNullException`, and `capacity` / `loadFactor` are validated exactly as the dictionaries. AOT-safe (no reflection).
11
19
- `CelerityMultiMapTests`, `CelerityMultiMapCollisionTests`, and `CelerityMultiMapEnumerationTests` — dedicated suites mirroring the `CelerityDictionary*` files for the grouping semantics: grouping Adds (including duplicate keys and duplicate values), the indexer/`TryGetValues` empty-on-miss group views, `Contains(key, value)` / `ContainsValue` / `CountValues`, both removal shapes (`Remove(key, value)` collapsing a key when its last value is removed, and `RemoveAll(key)`), `Clear`, `Count` vs `ValueCount`, `AddRange`, the out-of-band default-key group for `string` (`null`), `int` (`0`), and `Guid` (`Guid.Empty`), the `IEnumerable<KeyValuePair<,>>` grouping constructor, and the `ILookup<,>` surface; collision tests under a constant hasher (and an identity hasher for the wrapped-cluster branch) that assert grouping stays distinct under a single probe chain and that backward-shift deletion on a collapsed key never orphans the others; and enumeration tests for the struct `Grouping` enumerator (default-key group first), the per-group `ValueGroup` enumerator, the `Keys` view, mid-enumeration mutation detection (including a value-only `Add`/`Remove` on an existing key), and the boxed `IEnumerable<IGrouping<,>>` / non-generic paths.
0 commit comments