jaffarCommon
Loading...
Searching...
No Matches
sequenceTrie.hpp
Go to the documentation of this file.
1#pragma once
2
23#include "exceptions.hpp"
24#include <algorithm>
25#include <array>
26#include <atomic>
27#include <cstdint>
28#include <mutex>
29#include <vector>
30
31namespace jaffarCommon
32{
33
34namespace sequenceTrie
35{
36
41template <typename Element>
43{
44public:
46 using nodeId_t = uint32_t;
47
49 static constexpr nodeId_t ROOT = 0;
50
52 static constexpr nodeId_t NONE = 0xFFFFFFFFu;
53
59 explicit SequenceTrie(uint32_t numShards = 1, uint32_t chunkSizeLog2 = 20)
60 : _chunkBits(chunkSizeLog2), _chunkSize(1u << chunkSizeLog2), _chunkMask((1u << chunkSizeLog2) - 1), _shards(numShards == 0 ? 1 : numShards)
61 {
62 for (auto& c : _chunks) c.store(nullptr, std::memory_order_relaxed);
63 for (auto& s : _shards) s.head = NONE;
64 _bump.store(0, std::memory_order_relaxed);
65
66 // Allocate ROOT (id 0). It carries a permanent self-reference so it is never recycled.
67 const nodeId_t root = allocNode(0);
68 Node& r = node(root);
69 r.parent = NONE;
70 r.element = Element{};
71 r.refCount.store(1, std::memory_order_relaxed);
72 }
73
75 {
76 for (auto& c : _chunks)
77 {
78 Node* p = c.load(std::memory_order_relaxed);
79 delete[] p;
80 }
81 }
82
83 SequenceTrie(const SequenceTrie&) = delete;
84 SequenceTrie& operator=(const SequenceTrie&) = delete;
85
95 nodeId_t extend(nodeId_t parent, Element element, uint32_t shard = 0)
96 {
97 const nodeId_t id = allocNode(shard);
98 Node& n = node(id);
99 n.parent = parent;
100 n.element = element;
101 n.refCount.store(1, std::memory_order_relaxed); // the caller's reference
102
103 // Account for the new child edge on the parent (kept alive by its children).
104 incRef(node(parent));
105 return id;
106 }
107
112 void acquire(nodeId_t id) { incRef(node(id)); }
113
122 void release(nodeId_t id, uint32_t shard = 0)
123 {
124 while (id != NONE)
125 {
126 Node& n = node(id);
127 // release-acquire so a thread that observes the count hit zero sees all prior writes to the node.
128 if (n.refCount.fetch_sub(1, std::memory_order_acq_rel) != 1) break; // still referenced -> stop
129
130 const nodeId_t parent = n.parent; // read before the slot is handed to the free list
131 freeNode(id, shard);
132 id = parent; // the freed node was a child of `parent`; drop that child edge too
133 }
134 }
135
141 template <typename OutElem = Element>
142 void reconstruct(nodeId_t id, std::vector<OutElem>& out) const
143 {
144 out.clear();
145 for (nodeId_t cur = id; cur != ROOT && cur != NONE;)
146 {
147 const Node& n = node(cur);
148 out.push_back(static_cast<OutElem>(n.element)); // widen the 16-bit stored element to the caller's type
149 cur = n.parent;
150 }
151 std::reverse(out.begin(), out.end());
152 }
153
159 size_t getDepth(nodeId_t id) const
160 {
161 size_t d = 0;
162 for (nodeId_t cur = id; cur != ROOT && cur != NONE; cur = node(cur).parent) d++;
163 return d;
164 }
165
171 size_t getAllocatedNodeCount() const { return _bump.load(std::memory_order_relaxed); }
172
177 size_t getApproxMemoryBytes() const { return getAllocatedNodeCount() * sizeof(Node); }
178
186 size_t getMaxMemoryBytes() const { return MAX_CHUNKS * (size_t)_chunkSize * sizeof(Node); }
187
188private:
189 struct Node
190 {
191 uint32_t parent;
192 Element element;
193 std::atomic<uint16_t> refCount;
196 };
197
198 // Fixed cap on chunks so the chunk-pointer array never reallocates (handles stay valid lock-free).
199 static constexpr size_t MAX_CHUNKS = 4096;
200
201 __attribute__((always_inline)) Node& node(nodeId_t id) const { return _chunks[id >> _chunkBits].load(std::memory_order_acquire)[id & _chunkMask]; }
202
207 __attribute__((always_inline)) void incRef(Node& n) const
208 {
209 if (n.refCount.fetch_add(1, std::memory_order_relaxed) == UINT16_MAX)
210 JAFFAR_THROW_RUNTIME("SequenceTrie node refCount overflowed 16 bits (out-degree + holders > %u); the "
211 "input set is too large for the 16-bit-refCount trie node.",
212 (unsigned)UINT16_MAX);
213 }
214
215 // Obtain a node slot: reuse one recycled into this shard's (thread-private) free list if available,
216 // otherwise bump-allocate a fresh id. The per-shard free list needs no atomics because a given shard
217 // is only ever touched by its owning thread -- removing the single-free-list contention that otherwise
218 // serializes all workers. Only the rare fresh-id bump (free list empty) touches a shared atomic.
219 nodeId_t allocNode(uint32_t shard)
220 {
221 FreeShard& fs = _shards[shard];
222 if (fs.head != NONE)
223 {
224 const nodeId_t top = fs.head;
225 fs.head = node(top).parent; // free-list "next" stored in parent
226 return top;
227 }
228
229 // Free list empty: bump-allocate a fresh id, faulting in its chunk on first use.
230 const nodeId_t id = (nodeId_t)_bump.fetch_add(1, std::memory_order_relaxed);
231 const size_t chunk = id >> _chunkBits;
232 if (chunk >= MAX_CHUNKS) JAFFAR_THROW_RUNTIME("SequenceTrie exceeded its maximum capacity (%zu chunks)", MAX_CHUNKS);
233 if (_chunks[chunk].load(std::memory_order_acquire) == nullptr) ensureChunk(chunk);
234 return id;
235 }
236
237 void freeNode(nodeId_t id, uint32_t shard)
238 {
239 FreeShard& fs = _shards[shard];
240 node(id).parent = fs.head; // push onto this shard's private stack
241 fs.head = id;
242 }
243
244 void ensureChunk(size_t chunk)
245 {
246 std::lock_guard<std::mutex> lock(_growMutex);
247 if (_chunks[chunk].load(std::memory_order_relaxed) != nullptr) return; // another thread won the race
248 Node* p = new Node[_chunkSize];
249 _chunks[chunk].store(p, std::memory_order_release);
250 }
251
254 struct alignas(64) FreeShard
255 {
256 nodeId_t head = NONE;
257 };
258
259 const uint32_t _chunkBits;
260 const uint32_t _chunkSize;
261 const uint32_t _chunkMask;
262
263 mutable std::array<std::atomic<Node*>, MAX_CHUNKS> _chunks;
264 std::atomic<uint64_t> _bump;
265 std::vector<FreeShard> _shards;
266 std::mutex _growMutex;
267};
268
269} // namespace sequenceTrie
270
271} // namespace jaffarCommon
A concurrent reference-counted prefix trie over sequences of Element.
Definition sequenceTrie.hpp:43
size_t getAllocatedNodeCount() const
Total node slots ever bump-allocated (high-water of simultaneously-live nodes, since freed nodes are ...
Definition sequenceTrie.hpp:171
void reconstruct(nodeId_t id, std::vector< OutElem > &out) const
Reconstructs the full sequence from ROOT to id, in root-first order.
Definition sequenceTrie.hpp:142
size_t getMaxMemoryBytes() const
Hard upper bound on the trie's node storage, in bytes: the most it can ever occupy before node alloca...
Definition sequenceTrie.hpp:186
uint32_t nodeId_t
Handle identifying a node (a stored sequence). Stable for the node's lifetime.
Definition sequenceTrie.hpp:46
nodeId_t extend(nodeId_t parent, Element element, uint32_t shard=0)
Appends element after parent, returning a handle to the new sequence.
Definition sequenceTrie.hpp:95
static constexpr nodeId_t NONE
Sentinel for "no node" (also the free-list terminator).
Definition sequenceTrie.hpp:52
size_t getDepth(nodeId_t id) const
Number of elements from ROOT to id (its depth in the trie).
Definition sequenceTrie.hpp:159
size_t getApproxMemoryBytes() const
Approximate resident memory of the trie's node storage, in bytes.
Definition sequenceTrie.hpp:177
static constexpr nodeId_t ROOT
The empty sequence. Always valid, never recycled; the base of every path.
Definition sequenceTrie.hpp:49
SequenceTrie(uint32_t numShards=1, uint32_t chunkSizeLog2=20)
Constructs an empty trie containing only ROOT.
Definition sequenceTrie.hpp:59
void acquire(nodeId_t id)
Adds one reference to id (when a new holder starts referencing it). Thread-safe.
Definition sequenceTrie.hpp:112
void release(nodeId_t id, uint32_t shard=0)
Drops one reference from id. Thread-safe.
Definition sequenceTrie.hpp:122
Contains common functions for exception throwing.
#define JAFFAR_THROW_RUNTIME(...)
Definition exceptions.hpp:22