Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to Celerity are documented here. This project follows [Keep
### Added

- **`Trie<TValue>`** in `Celerity.Collections` — an ordered prefix tree mapping `string` keys to values, filling a BCL gap (.NET ships no trie). `GetByPrefix` lists every entry whose key starts with a prefix in `O(prefix + matches)` and in ascending key order, and `TryGetLongestPrefix` finds the longest stored key that is a prefix of a query in `O(query)` — the autocomplete, longest-prefix-routing, and ordered-iteration workloads a `Dictionary<string, TValue>` can only answer with an `O(n)` scan plus a `StartsWith` per key. Exact `Add` / `TryGetValue` favour a `Dictionary`, so the trie earns its place on the prefix operations. Implements `IReadOnlyDictionary<string, TValue?>`; not thread-safe. Closes [#285](https://github.com/marius-bughiu/Celerity/issues/285).
- **`SparseSet`** in `Celerity.Collections` — a bounded-universe `[0, Universe)` integer set (the Briggs–Torczon sparse set), filling a BCL gap. Where the set is cleared and rebuilt often — "visited" sets in graph traversal, ECS, sweep-line — it beats `HashSet<int>` with an `O(1)` `Clear` (the backing arrays are left untouched) and dense iteration over just the present elements. Costs `O(Universe)` memory, stores only values in `[0, Universe)`, and implements `ISet<int>`; an opt-in specialized type. Closes [#287](https://github.com/marius-bughiu/Celerity/issues/287).

### Fixed

Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ Standalone libraries built **on top of** Celerity — each solves a real problem
- `IntSet` / `LongSet` — `int` / `long`-keyed set specializations.
- `SmallSet<T>` — flat-array, linear-scan set for the very-small (`n <= ~16`) case. No hasher; the default element is stored inline. The set counterpart of `SmallDictionary`.
- `EnumSet<TEnum>` — bit-vector set for enum keys (the .NET `EnumSet`): membership is a single bit test and set algebra is word-wise bitwise ops, with no hashing or boxing. Enumerates in ascending underlying-value order.
- `SparseSet` — bounded-universe integer set (Briggs–Torczon sparse set): `O(1)` `Clear` that leaves the backing arrays untouched, plus dense, cache-friendly iteration — for clear-and-rebuild "visited" sets over ids in `[0, N)` (graph traversal, ECS, sweep-line). Costs `O(Universe)` memory.

The mutable sets (`CeleritySet`, `SwissSet`, `RobinHoodSet`, `HashCachingSet`, `IntSet`, `LongSet`, `SmallSet`, `EnumSet`) all implement **`ISet<T>`** — the full `HashSet<T>` set-algebra surface (`UnionWith` / `IntersectWith` / `ExceptWith` / `SymmetricExceptWith` and the `IsSubsetOf` / `IsSupersetOf` / `Overlaps` / `SetEquals` query family, plus `CopyTo`) with BCL semantics — so they drop in wherever a `HashSet<T>` is used.
The mutable sets (`CeleritySet`, `SwissSet`, `RobinHoodSet`, `HashCachingSet`, `IntSet`, `LongSet`, `SmallSet`, `EnumSet`, `SparseSet`) all implement **`ISet<T>`** — the full `HashSet<T>` set-algebra surface (`UnionWith` / `IntersectWith` / `ExceptWith` / `SymmetricExceptWith` and the `IsSubsetOf` / `IsSupersetOf` / `Overlaps` / `SetEquals` query family, plus `CopyTo`) with BCL semantics — so they drop in wherever a `HashSet<T>` is used. (The bounded-domain sets, `EnumSet` and `SparseSet`, are the exception to "drop in anywhere": they store only values in their fixed domain, so a mutating op that must add an out-of-domain value throws.)

**Caches**

Expand Down Expand Up @@ -222,7 +223,7 @@ Console.WriteLine(queued.ContainsKey(Priority.Normal)); // False — a single bi
</details>

<details>
<summary><b>Sets</b> — IntSet, CeleritySet, SwissSet, RobinHoodSet, HashCachingSet, FrozenCeleritySet, SmallSet, EnumSet</summary>
<summary><b>Sets</b> — IntSet, CeleritySet, SwissSet, RobinHoodSet, HashCachingSet, FrozenCeleritySet, SmallSet, EnumSet, SparseSet</summary>

```csharp
var seen = new IntSet();
Expand Down Expand Up @@ -294,6 +295,15 @@ Console.WriteLine(granted.IsSupersetOf(required)); // False — word-wise subset
granted.UnionWith(required); // one bitwise OR
```

`SparseSet` is the bounded-universe integer set — the classic Briggs–Torczon sparse set (a dense value array + a sparse index array). Over a fixed universe `[0, Universe)` chosen at construction, `Add` / `Contains` / `Remove` are `O(1)` with no hashing, but the point of the type is what `HashSet<int>` can't match: `Clear()` is `O(1)` (it resets the count without scanning or clearing the backing arrays, versus zeroing the whole table) and iteration is a dense, contiguous scan over exactly the present elements. That is the winning shape for clear-and-rebuild "visited" sets — graph BFS/DFS, ECS entity membership, sweep-line — where the set is emptied every iteration. The cost is `O(Universe)` memory and non-negative-values-only: a value outside `[0, Universe)` throws on `Add` and reads as absent on `Contains` / `Remove`. It is an opt-in specialized type, not a `HashSet<int>` replacement — for an unbounded or huge-and-sparse key space, reach for `IntSet`.

```csharp
var visited = new SparseSet(nodeCount); // universe = ids in [0, nodeCount)
visited.Add(start);
Console.WriteLine(visited.TryAdd(start)); // False — already seen, unchanged
visited.Clear(); // O(1) — ready for the next traversal
```

</details>

<details>
Expand Down Expand Up @@ -495,6 +505,7 @@ Each type buys a different tradeoff. Find your workload below; if it isn't here,
| Dictionary keyed by a small **enum** — config-by-enum, per-state data, enum→handler tables | `EnumMap<TEnum, TValue>` | Dense array indexed on the enum's underlying value (the .NET `EnumMap`): `this[key]` / `TryGetValue` / `Add` / `Remove` are a single direct array index — no hashing, no probing, no collisions — and a full sweep is a linear array walk. The dictionary counterpart of `EnumSet`; enumerates ascending by value. For enums whose members are small non-negative integers (the default); negative or sparse `[Flags]` enums are unsupported — use `CelerityDictionary<TEnum, TValue, THasher>` there. |
| Tiny set (`n <= ~16`) that stays small — per-scope "seen" sets, small membership guards, deduping a handful of items | `SmallSet<T>` | The set counterpart of `SmallDictionary`: flat-array linear scan beats hashing at small `n`, no hasher to pick, the default element is stored inline. Implements `ISet<T>`. Degrades to `O(n)` for large sets, so only when instances stay small. |
| Set of **enum** values — flag sets, permission sets, state sets over a small enum | `EnumSet<TEnum>` | Bit-vector set indexed on the enum's underlying value (the .NET `EnumSet`): `Add` / `Contains` / `Remove` are a single bit op — no hashing, no boxing — and set algebra between two `EnumSet`s is a word-wise bitwise `OR` / `AND` / `XOR`. Enumerates ascending by value; `All()` builds the full universe. For enums whose members are small non-negative integers (the default); negative or sparse `[Flags]` enums are unsupported — use `CeleritySet<TEnum, THasher>` there. |
| Set of small **non-negative ints** over a bounded range that is **cleared & rebuilt often** — "visited" sets in graph BFS/DFS, ECS entity membership, sweep-line | `SparseSet` | Briggs–Torczon sparse set (dense value array + sparse index array): `O(1)` `Clear` that leaves the backing arrays untouched (vs `HashSet<int>` zeroing its table) and dense, cache-friendly iteration over just the present elements. `Add` / `Contains` / `Remove` are `O(1)`, no hashing. Costs `O(Universe)` memory and stores only values in `[0, Universe)`; for an unbounded or huge-and-sparse key space use `IntSet` / `HashSet<int>`. |
| 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`. |
Expand Down
128 changes: 128 additions & 0 deletions docs/api/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,134 @@ foreach (var p in granted) { /* ascending: Read, Write, Execute */ }

---

## SparseSet

```csharp
public class SparseSet : ISet<int>
```

A set of **non-negative integers over a bounded universe** `[0, Universe)`, backed by
the classic **Briggs–Torczon sparse-set representation**: a *dense* array holding the
present values contiguously, paired with a *sparse* array — indexed by value — that
points each present value back at its slot in the dense array. Membership is the
round-trip `sparse[v] < Count && dense[sparse[v]] == v`, which is correct even for a *stale*
sparse entry — one left over from before a `Clear`, or the zero a never-written slot still
holds. That single fact is what buys the type its two wins over `HashSet<int>`:

- **`Clear()` is `O(1)`** — it resets the count *without scanning or clearing the
backing arrays*. `HashSet<int>.Clear()` is `O(capacity)` (it zeroes the whole entry table).
This is the headline: per-frame / per-query "visited" sets in graph traversal (BFS/DFS), ECS
entity membership, register-allocation liveness, and sweep-line algorithms clear on every
iteration.
- **Dense, cache-friendly iteration** — present elements live contiguously in `[0, Count)`
of the dense array, so enumeration is a linear scan over exactly `Count` ints with no
empty-slot skipping.

`Add` / `Contains` / `Remove` are each `O(1)` with **no hashing**, no probe chain, and no
per-element allocation — a direct array index and the round-trip check. There is **no
hasher** (and so no `THasher` type parameter).

The trade-offs, stated honestly:

- The sparse index array is **`O(Universe)` memory**, sized once at construction. The type
is worth it when the universe is bounded and the set is cleared / rebuilt / iterated
often — not as a general `HashSet<int>` replacement. For an unbounded or huge-and-sparse
key space, use [`IntSet`](#intset) / `HashSet<int>`.
- It stores **only non-negative values below `Universe`**. A value outside `[0, Universe)`
is rejected by `Add` / `TryAdd` with `ArgumentOutOfRangeException`, and reported as absent
by `Contains` / `Remove` (the bounded-universe analogue of `EnumSet`).
- `Remove` moves the last dense element into the vacated slot (an `O(1)` swap), so the
relative order of the surviving elements is not preserved. Enumeration order is
unspecified in general.

It implements `ISet<int>` (and therefore `ICollection<int>` / `IEnumerable<int>`), ships an
allocation-free struct enumerator, and accepts an `IEnumerable<int>` source at construction.

### Constructors

```csharp
SparseSet(int universe)
SparseSet(int universe, IEnumerable<int> source)
```

- `universe` is the **exclusive upper bound** of storable values; the set can hold any
non-negative integer strictly less than it. It sizes the sparse index array once, so
it is the dominant memory cost — choose it to match the actual value range. `0` creates
a set that can store nothing.
- Throws `ArgumentOutOfRangeException` for a negative `universe`. There is **no
`loadFactor`** parameter.
- The `source` constructor pre-sizes the dense array from an `ICollection<int>` source,
silently deduplicates (matching BCL `HashSet<int>(IEnumerable<int>)` semantics), throws
`ArgumentNullException` if `source` is `null` (the null check beats the universe
validation), and throws `ArgumentOutOfRangeException` if any source value is outside
`[0, universe)`.

### Methods

- `void Add(int item)` — throws `ArgumentException` on duplicate, `ArgumentOutOfRangeException` out of range.
- `bool TryAdd(int item)` — `true` on success, `false` if already present; throws out of range.
- `bool Contains(int item)` — `O(1)`; `false` for an out-of-range value.
- `bool Remove(int item)` — `O(1)` swap-removal; `false` for an absent or out-of-range value.
- `void Clear()` — **`O(1)`**, touches no memory; the set stays reusable.
- `int EnsureCapacity(int capacity)` — grow the dense array to hold at least `capacity`
elements (clamped to `Universe`), returning the resulting dense-array length. Throws
`ArgumentOutOfRangeException` on a negative capacity.
- `void TrimExcess()` / `void TrimExcess(int capacity)` — shrink the dense array to exactly
the current `Count` (or `capacity`). `TrimExcess(capacity)` throws if `capacity < Count`
or `capacity > Universe`. The sparse index array is unaffected.
- `int Count { get; }`, `int Universe { get; }`
- `Enumerator GetEnumerator()` — allocation-free struct enumerator over the dense array.
- `void CopyTo(int[] array, int arrayIndex)` — matches `HashSet<int>.CopyTo` argument validation.

### Set operations (`ISet<int>`)

The full BCL `HashSet<int>` set-algebra surface is available and follows `HashSet<int>`
semantics exactly within the universe (duplicate-tolerant `other`, self-aliasing
`other == this`):

- **Mutating:** `UnionWith`, `IntersectWith`, `ExceptWith`, `SymmetricExceptWith`.
- **Query:** `IsSubsetOf`, `IsProperSubsetOf`, `IsSupersetOf`, `IsProperSupersetOf`, `Overlaps`, `SetEquals`.

Each throws `ArgumentNullException` when `other` is `null`. The one bounded-universe caveat:
a **mutating** operation that would *add* a value outside `[0, Universe)` (e.g. `UnionWith`
with such an element) throws `ArgumentOutOfRangeException` rather than silently growing an
unbounded set. Query operations tolerate out-of-range values in `other` (they simply read
as absent). As with the other sets, `ISet<int>.Add(int)` returns `bool` (equivalent to
`TryAdd`), the concrete `public void Add(int)` keeps its throw-on-duplicate behaviour, and
`ICollection<int>.Add(int)` ignores duplicates.

### Usage example

```csharp
using Celerity.Collections;

// A BFS "visited" set over a graph whose nodes are ids in [0, nodeCount).
var visited = new SparseSet(nodeCount);

for (int start = 0; start < nodeCount; start++)
{
visited.Clear(); // O(1) — no memory touched, ready for the next traversal
var queue = new Queue<int>();
queue.Enqueue(start);
visited.Add(start);

while (queue.Count > 0)
{
int node = queue.Dequeue();
foreach (int next in Neighbors(node))
{
if (visited.TryAdd(next)) // false if already seen
queue.Enqueue(next);
}
}

// Iterate exactly the reached nodes — a dense, contiguous scan.
Process(visited);
}
```

---

## EnumMap&lt;TEnum, TValue&gt;

```csharp
Expand Down
24 changes: 24 additions & 0 deletions src/Celerity.AotSmokeTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,30 @@ void Check(bool condition, string message)
Check(maxHeap.Dequeue() == "b", "IndexedPriorityQueue custom comparer (max-heap)");
}

// SparseSet — bounded-universe sparse integer set (Briggs–Torczon). Exercise add /
// contains / swap-remove, the out-of-range rejection, the O(1) clear-then-reuse path
// (which must reject stale sparse entries), and the dense-array enumerator.
{
var ss = new SparseSet(64);
for (int i = 0; i < 10; i++) ss.Add(i);
Check(ss.Count == 10 && ss.Universe == 64 && ss.Contains(0) && ss.Contains(9), "SparseSet add + contains");
Check(!ss.TryAdd(5), "SparseSet.TryAdd duplicate");
Check(!ss.Contains(64) && !ss.Contains(-1), "SparseSet out-of-range reads absent");
Check(ss.Remove(5) && !ss.Contains(5) && ss.Contains(9), "SparseSet swap-remove keeps survivors");

ss.Clear();
Check(ss.Count == 0 && !ss.Contains(0) && !ss.Contains(9), "SparseSet O(1) clear rejects stale entries");
ss.Add(9); // 9 was present before Clear — must not false-positive until re-added
Check(ss.Count == 1 && ss.Contains(9) && !ss.Contains(0), "SparseSet reusable after clear");

var reached = new SparseSet(128, new[] { 3, 3, 7, 1, 7 }); // dedupes
var seen = new List<int>();
foreach (int x in reached) seen.Add(x);
Check(reached.Count == 3 && seen.Count == 3, "SparseSet source ctor dedupe + enumeration");
((ISet<int>)reached).UnionWith(new[] { 1, 2 });
Check(reached.Count == 4 && reached.Contains(2), "SparseSet ISet<int> union within universe");
}

// SmallDictionary — flat-array, linear-scan dictionary (default key inline, no
// hasher). Exercise the indexer, TryAdd/Add, TryGetValue, Remove, the swap-remove
// path, the inline default/zero key, and the struct enumerator.
Expand Down
1 change: 1 addition & 0 deletions src/Celerity.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal class Program
typeof(LongSetBenchmark),
typeof(SmallSetBenchmark),
typeof(EnumSetBenchmark),
typeof(SparseSetBenchmark),
typeof(BloomFilterBenchmark),
typeof(CuckooFilterBenchmark),
typeof(XorFilterBenchmark),
Expand Down
Loading
Loading