Coverage Report

Created: 2026-08-24 19:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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