src/util/sharded-stack.cpp
Line | Count | Source |
1 | | #include <silk/util/sharded-stack.h> |
2 | | |
3 | | #include <silk/util/assert.h> |
4 | | #include <silk/util/sanitizers.h> |
5 | | |
6 | | #include <new> |
7 | | |
8 | | // Suppress warnings emitted by librseq headers: volatile assignment in rseq_cs |
9 | | // and unused parameters in the asm stubs. |
10 | | #pragma clang diagnostic push |
11 | | #pragma clang diagnostic ignored "-Wdeprecated-volatile" |
12 | | #pragma clang diagnostic ignored "-Wunused-parameter" |
13 | | #include <rseq/rseq.h> |
14 | | #pragma clang diagnostic pop |
15 | | |
16 | | namespace silk |
17 | | { |
18 | | |
19 | | ShardedStackBase::ShardedStackBase(uint32_t batchSize) noexcept |
20 | 539 | : batchSize(batchSize) |
21 | 539 | , processorCount(getProcessorCount()) |
22 | 539 | , processorState(std::make_unique<ProcessorState[]>(processorCount)) |
23 | 539 | { |
24 | 9.16k | for (uint32_t i = 0; i < processorCount; ++i8.62k ) |
25 | 8.62k | { |
26 | 8.62k | FreeList * freeList = acquireFreeList(); |
27 | 8.62k | SILK_ASSERT(freeList); |
28 | 8.62k | emptyFreeLists.push(freeList); |
29 | 8.62k | } |
30 | 539 | } |
31 | | |
32 | | ShardedStackBase::~ShardedStackBase() noexcept |
33 | 539 | { |
34 | 5.36k | while (FreeList * freeList = emptyFreeLists.pop()) |
35 | 4.82k | { |
36 | 4.82k | releaseFreeList(freeList); |
37 | 4.82k | } |
38 | | |
39 | 6.01k | while (FreeList * freeList = fullFreeLists.pop()) |
40 | 5.47k | { |
41 | 5.47k | releaseFreeList(freeList); |
42 | 5.47k | } |
43 | 539 | } |
44 | | |
45 | | void ShardedStackBase::push(StackEntry * entry) noexcept |
46 | 70.0k | { |
47 | 70.0k | for (;;) |
48 | 72.5k | { |
49 | 72.5k | uint32_t cpu = rseq_cpu_start(); |
50 | 72.5k | ProcessorState * state = &processorState[cpu]; |
51 | | |
52 | 72.5k | uint32_t count = state->count.load(std::memory_order_relaxed); |
53 | 72.5k | if (count < batchSize) |
54 | 68.4k | { |
55 | | // Prepare the link before the critical section. |
56 | | // Release ordering publishes all writes to entry's content (e.g. deinitialisation) |
57 | | // before the entry becomes visible to a concurrent pop. |
58 | | // The matching acquire is in pop after the rseq commit. |
59 | 68.4k | StackEntry * oldHead = state->head.load(std::memory_order_relaxed); |
60 | 68.4k | entry->next.store(oldHead, std::memory_order_release); |
61 | | |
62 | | // Fast path: rseq critical section, zero atomic instructions. |
63 | | // |
64 | | // load head; if head != oldHead -> ne (head changed, retry); |
65 | | // commit: head = entry. |
66 | | // |
67 | | // The ne check catches the case where we migrated to a new CPU between |
68 | | // reading oldHead and the commit -- the new CPU's head is unlikely to |
69 | | // equal oldHead, and even if it does the rseq abort (ret == -1) fires |
70 | | // on migration before the commit can execute. |
71 | 68.4k | TSAN_IGNORE_BEGIN(); |
72 | 68.4k | int ret = rseq_load_cbne_store__ptr( |
73 | 68.4k | RSEQ_MO_RELAXED, // only supported memory order |
74 | 68.4k | RSEQ_PERCPU_CPU_ID, // identify CPU via cpu_id field |
75 | 68.4k | state->headPtr(), // per-CPU head pointer |
76 | 68.4k | reinterpret_cast<intptr_t>(oldHead), // return 1 if head changed |
77 | 68.4k | reinterpret_cast<intptr_t>(entry), // store entry as new head on success |
78 | 68.4k | static_cast<int>(cpu)); // expected CPU ID |
79 | 68.4k | TSAN_IGNORE_END(); |
80 | | |
81 | 68.4k | if (ret == 0) |
82 | 69.1k | { |
83 | | // count is a heuristic; a stale increment triggers the slow path |
84 | | // one operation early at worst. count was captured before the rseq |
85 | | // so no RMW needed -- plain store avoids the LOCK prefix. |
86 | 69.1k | state->count.store(count + 1, std::memory_order_relaxed); |
87 | 69.1k | return; |
88 | 69.1k | } |
89 | | |
90 | | // ret == 1 (head changed) or -1 (preempted): retry. |
91 | 68.4k | } |
92 | 4.08k | else |
93 | 4.08k | { |
94 | | // Slow path: per-CPU list full, move it to the global pool. |
95 | 4.08k | flush(state); |
96 | 4.08k | } |
97 | 72.5k | } |
98 | 70.0k | } |
99 | | |
100 | | void ShardedStackBase::flush(ProcessorState * state) noexcept |
101 | 5.78k | { |
102 | | // acq_rel: acquire observes any in-progress rseq commit; |
103 | | // release publishes the null so subsequent rseq reads see an empty list. |
104 | 5.78k | StackEntry * head = state->head.exchange(nullptr, std::memory_order_acq_rel); |
105 | 5.78k | state->count.store(0, std::memory_order_relaxed); |
106 | | |
107 | 5.78k | if (head) |
108 | 2.92k | { |
109 | 2.92k | StackEntry * tail = head; |
110 | 50.8k | while (StackEntry * next = tail->next.load(std::memory_order_relaxed)) |
111 | 47.9k | { |
112 | 47.9k | tail = next; |
113 | 47.9k | } |
114 | 2.92k | pushBatch(head, tail); |
115 | 2.92k | } |
116 | 5.78k | } |
117 | | |
118 | | StackEntry * ShardedStackBase::pop() noexcept |
119 | 65.0k | { |
120 | 65.0k | for (;;) |
121 | 67.1k | { |
122 | 67.1k | uint32_t cpu = rseq_cpu_start(); |
123 | 67.1k | ProcessorState * state = &processorState[cpu]; |
124 | | |
125 | 67.1k | uint32_t count = state->count.load(std::memory_order_relaxed); |
126 | | |
127 | | // Fast path: rseq critical section, zero atomic instructions. |
128 | | // |
129 | | // load head; if null -> eq (slow path); |
130 | | // save head to entry; load head->next (voffp=0); commit: head = next. |
131 | | // |
132 | | // If preempted before the commit the kernel restarts from the top of |
133 | | // the loop (ret == -1). The commit is a plain store; no CAS needed. |
134 | 67.1k | intptr_t entry = 0; |
135 | 67.1k | TSAN_IGNORE_BEGIN(); |
136 | 67.1k | int ret = rseq_load_cbeq_store_add_load_store__ptr( |
137 | 67.1k | RSEQ_MO_RELAXED, // only supported memory order |
138 | 67.1k | RSEQ_PERCPU_CPU_ID, // identify CPU via cpu_id field |
139 | 67.1k | state->headPtr(), // per-CPU head pointer |
140 | 67.1k | 0, // return 1 if head == NULL (empty list) |
141 | 67.1k | 0, // byte offset from head to next pointer; StackEntry::next is at offset 0 |
142 | 67.1k | &entry, // receives the old head value (the popped entry) |
143 | 67.1k | static_cast<int>(cpu)); // expected CPU ID |
144 | 67.1k | TSAN_IGNORE_END(); |
145 | | |
146 | 67.1k | if (ret == 0) |
147 | 64.9k | { |
148 | | // Acquire synchronizes with the release store of entry->next in push, |
149 | | // establishing happens-before: all writes before push are visible here. |
150 | 64.9k | reinterpret_cast<StackEntry *>(entry)->next.load(std::memory_order_acquire); |
151 | | |
152 | | // count is a heuristic; a stale decrement triggers the slow path |
153 | | // one operation early at worst. count was captured before the rseq |
154 | | // so no RMW needed -- plain store avoids the LOCK prefix. |
155 | 64.9k | state->count.store(count - 1, std::memory_order_relaxed); |
156 | 64.9k | return reinterpret_cast<StackEntry *>(entry); |
157 | 64.9k | } |
158 | | |
159 | 2.20k | if (ret == 1) |
160 | 2.92k | { |
161 | | // Slow path: per-CPU list empty, pull a batch from the global pool. |
162 | 2.92k | if (!refill(state)) |
163 | 669 | { |
164 | 669 | return nullptr; |
165 | 669 | } |
166 | 2.25k | continue; |
167 | 2.92k | } |
168 | | |
169 | | // ret == -1 (preempted): retry. |
170 | 2.20k | } |
171 | 65.0k | } |
172 | | |
173 | | bool ShardedStackBase::refill(ProcessorState * state) noexcept |
174 | 2.91k | { |
175 | | // Slow path for pop: pull a batch from the global pool and install it as the |
176 | | // per-CPU list. Returns true if at least one entry is now available. |
177 | 2.91k | FreeList * freeList = fullFreeLists.pop(); |
178 | 2.91k | if (!freeList) [[unlikely]] |
179 | 668 | { |
180 | 668 | return false; |
181 | 668 | } |
182 | | |
183 | 2.24k | StackEntry * newHead = freeList->entries.popAll(); |
184 | 2.24k | emptyFreeLists.push(freeList); |
185 | | |
186 | 2.24k | if (!newHead) |
187 | 0 | { |
188 | 0 | return false; |
189 | 0 | } |
190 | | |
191 | | // Walk the chain to find the tail and actual count. Both are needed below: |
192 | | // the tail for pushBatch on CAS failure, the count to correctly initialize |
193 | | // ProcessorState::count (which may be less than batchSize if the FreeList |
194 | | // was partially filled by an explicit flush rather than a full overflow). |
195 | 2.24k | StackEntry * tail = newHead; |
196 | 2.24k | uint32_t actualCount = 1; |
197 | 50.6k | while (StackEntry * next = tail->next.load(std::memory_order_relaxed)) |
198 | 48.3k | { |
199 | 48.3k | tail = next; |
200 | 48.3k | actualCount++; |
201 | 48.3k | } |
202 | | |
203 | | // Install the batch as the per-CPU list. Use CAS rather than a plain store: |
204 | | // if we migrated between entering the slow path and reaching this point, |
205 | | // another thread may have pushed to this CPU's head concurrently, and a |
206 | | // plain store would clobber that entry. |
207 | 2.24k | StackEntry * expected = state->head.load(std::memory_order_relaxed); |
208 | 2.24k | for (;;) |
209 | 2.33k | { |
210 | 2.33k | if (expected) |
211 | 0 | { |
212 | 0 | pushBatch(newHead, tail); |
213 | 0 | break; |
214 | 0 | } |
215 | | |
216 | 2.33k | if (state->head.compare_exchange_weak(expected, newHead, std::memory_order_relaxed, std::memory_order_relaxed)) |
217 | 2.34k | { |
218 | 2.34k | state->count.store(actualCount, std::memory_order_relaxed); |
219 | 2.34k | break; |
220 | 2.34k | } |
221 | 2.33k | } |
222 | | |
223 | 2.24k | return true; |
224 | 2.24k | } |
225 | | |
226 | | void ShardedStackBase::pushBatch(StackEntry * head, StackEntry * tail) noexcept |
227 | 11.5k | { |
228 | 11.5k | FreeList * freeList = emptyFreeLists.pop(); |
229 | 11.5k | if (!freeList) [[unlikely]] |
230 | 1.67k | { |
231 | 1.67k | freeList = acquireFreeList(); |
232 | 1.67k | SILK_ASSERT(freeList); |
233 | 1.67k | } |
234 | 11.5k | freeList->entries.pushAll(head, tail); |
235 | 11.5k | fullFreeLists.push(freeList); |
236 | 11.5k | } |
237 | | |
238 | | void ShardedStackBase::flush() noexcept |
239 | 204 | { |
240 | 3.46k | for (uint32_t i = 0; i < processorCount; ++i3.26k ) |
241 | 3.26k | { |
242 | 3.26k | flush(&processorState[i]); |
243 | 3.26k | } |
244 | 204 | } |
245 | | |
246 | | void ShardedStackBase::drain(DrainCallback * callback, void * ctx) noexcept |
247 | 204 | { |
248 | 204 | flush(); |
249 | | |
250 | 3.91k | while (FreeList * freeList = fullFreeLists.pop()) |
251 | 3.71k | { |
252 | 3.71k | StackEntry * entry = freeList->entries.popAll(); |
253 | 86.4k | while (entry) |
254 | 82.7k | { |
255 | 82.7k | StackEntry * next = entry->next.load(std::memory_order_relaxed); |
256 | 82.7k | callback(entry, ctx); |
257 | 82.7k | entry = next; |
258 | 82.7k | } |
259 | 3.71k | emptyFreeLists.push(freeList); |
260 | 3.71k | } |
261 | 204 | } |
262 | | |
263 | | ShardedStackBase::FreeList * ShardedStackBase::acquireFreeList() noexcept |
264 | 10.2k | { |
265 | | // Ignore non-atomic initialization |
266 | 10.2k | TSAN_IGNORE_BEGIN(); |
267 | 10.2k | FreeList * freeList = new (std::nothrow) FreeList; |
268 | 10.2k | TSAN_IGNORE_END(); |
269 | 10.2k | return freeList; |
270 | 10.2k | } |
271 | | |
272 | | void ShardedStackBase::releaseFreeList(FreeList * freeList) noexcept |
273 | 10.2k | { |
274 | 10.2k | delete freeList; |
275 | 10.2k | } |
276 | | |
277 | | } // namespace silk |