xenium
stamp_it.hpp
1 //
2 // Copyright (c) 2018-2020 Manuel Pöter.
3 // Licensed under the MIT License. See LICENSE file in the project root for full license information.
4 //
5 
6 #ifndef STAMP_IT_IMPL
7  #error "This is an impl file and must not be included directly!"
8 #endif
9 
10 #include <xenium/aligned_object.hpp>
11 #include <xenium/detail/port.hpp>
12 #include <xenium/reclamation/detail/perf_counter.hpp>
13 #include <xenium/reclamation/detail/thread_block_list.hpp>
14 
15 #include <algorithm>
16 #include <limits>
17 
18 #ifdef _MSC_VER
19  #pragma warning(push)
20  #pragma warning(disable : 4324) // structure was padded due to alignment specifier
21 #endif
22 
23 namespace xenium::reclamation {
24 
25 struct stamp_it::thread_control_block :
26  aligned_object<thread_control_block, 1 << MarkBits>,
27  detail::thread_block_list<thread_control_block>::entry {
28  using concurrent_ptr = std::atomic<marked_ptr<thread_control_block, MarkBits>>;
29 
30  concurrent_ptr prev;
31  concurrent_ptr next;
32 
33  std::atomic<stamp_t> stamp;
34 
35 #ifdef WITH_PERF_COUNTER
36  performance_counters counters;
37  friend class thread_order_queue;
38 #endif
39 };
40 
41 class stamp_it::thread_order_queue {
42 public:
43  using marked_ptr = xenium::marked_ptr<thread_control_block, MarkBits>;
44  using concurrent_ptr = std::atomic<marked_ptr>;
45 
46  thread_order_queue() {
47  head = new thread_control_block();
48  tail = new thread_control_block();
49  tail->next.store(head, std::memory_order_relaxed);
50  tail->stamp.store(StampInc, std::memory_order_relaxed);
51  head->prev.store(tail, std::memory_order_relaxed);
52  head->stamp.store(StampInc, std::memory_order_relaxed);
53  }
54 
55  void push(thread_control_block* block) {
56  INC_PERF_CNT(block->counters.push_calls);
57  PERF_COUNTER(iterations, block->counters.push_iterations)
58 
59  // (1) - this release-store synchronizes-with the acquire-loads (7, 8, 20, 24, 25, 29, 31, 32)
60  block->next.store(make_clean_marked(head, block->next), std::memory_order_release);
61 
62  marked_ptr head_prev = head->prev.load(std::memory_order_relaxed);
63  marked_ptr my_prev;
64  size_t stamp;
65  for (;;) {
66  iterations.inc();
67 
68  assert((head_prev.mark() & DeleteMark) == 0 && "head must never be marked");
69  marked_ptr head_prev2 = head->prev.load(std::memory_order_relaxed);
70  if (head_prev != head_prev2) {
71  head_prev = head_prev2;
72  continue;
73  }
74 
75  // fetch a new stamp and set the PendingPush flag
76  // (2) - this seq_cst-fetch-add enforces a total order with (12)
77  // and synchronizes-with the acquire-loads (19, 23)
78  stamp = head->stamp.fetch_add(StampInc, std::memory_order_seq_cst);
79  auto pending_stamp = stamp - (StampInc - PendingPush);
80  assert((pending_stamp & PendingPush) && !(pending_stamp & NotInList));
81 
82  // We need release-semantic here to establish synchronize-with relation with
83  // the load in save_next_as_last_and_move_next_to_next_prev.
84  // (3) - this release-store synchronizes-with the acquire-loads (19, 23, 30)
85  block->stamp.store(pending_stamp, std::memory_order_release);
86 
87  if (head->prev.load(std::memory_order_relaxed) != head_prev) {
88  continue;
89  }
90 
91  my_prev = make_clean_marked(head_prev.get(), block->prev);
92 
93  // (4) - this release-store synchronizes-with the acquire-loads (15, 17, 18, 22, 26)
94  block->prev.store(my_prev, std::memory_order_release);
95 
96  // (5) - in this acq_rel-CAS the:
97  // - acquire-load synchronizes-with the release-stores (5, 21, 28)
98  // - release-store synchronizes-with the acquire-loads (5, 15, 18, 22)
99  if (head->prev.compare_exchange_weak(
100  head_prev, make_marked(block, head_prev), std::memory_order_acq_rel, std::memory_order_relaxed)) {
101  break;
102  }
103  // Back-Off
104  }
105 
106  // reset the PendingPush flag
107  // (6) - this release-store synchronizes-with the acquire-load (19, 23, 30)
108  block->stamp.store(stamp, std::memory_order_release);
109 
110  // try to update our successor's next pointer
111  // (7) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
112  auto link = my_prev->next.load(std::memory_order_acquire);
113  for (;;) {
114  if (link.get() == block || ((link.mark() & DeleteMark) != 0) ||
115  block->prev.load(std::memory_order_relaxed) != my_prev) {
116  break; // our successor is in the process of getting removed or has been removed already -> never mind
117  }
118 
119  assert(link.get() != tail);
120 
121  // (8) - in this CAS the:
122  // - release-store synchronizes-with the acquire-loads (7, 8, 14, 20, 24, 25, 29, 31, 32)
123  // - acquire-reload synchronizes-with the release-stores (1, 8, 27)
124  if (my_prev->next.compare_exchange_weak(
125  link, make_marked(block, link), std::memory_order_release, std::memory_order_acquire)) {
126  break;
127  }
128  // Back-Off
129  }
130  }
131 
132  bool remove(marked_ptr block) {
133  INC_PERF_CNT(block->counters.remove_calls);
134 
135  // We need acq-rel semantic here to ensure the happens-before relation
136  // between the remove operation and the reclamation of any node.
137  // - acquire to establish sychnronize-with relation with previous blocks
138  // that removed themselves by updating our prev.
139  // - release to establish synchronize-with relation with other threads
140  // that potentially remove our own block before we can do so.
141 
142  // (9) - in this acq_rel CAS the:
143  // - acquire-load synchronizes-with the release-stores (21, 28)
144  // - release-store synchronizes-with the acquire-loads (15, 17, 18, 22, 26)
145  marked_ptr prev = set_mark_flag(block->prev, std::memory_order_acq_rel);
146  marked_ptr next = set_mark_flag(block->next, std::memory_order_relaxed);
147 
148  bool fully_removed = remove_from_prev_list(prev, block, next);
149  if (!fully_removed) {
150  remove_from_next_list(prev, block, next);
151  }
152 
153  auto stamp = block->stamp.load(std::memory_order_relaxed);
154  assert((stamp & (PendingPush | NotInList)) == 0);
155  // set the NotInList flag to signal that this block is no longer part of the queue
156  block->stamp.store(stamp + NotInList, std::memory_order_relaxed);
157 
158  bool wasTail = block->prev.load(std::memory_order_relaxed).get() == tail;
159  if (wasTail) {
160  // since the stamps of the blocks between tail and head are strong monotonically increasing we can
161  // call update_tail_stamp with the next higher stamp (i.e., stamp + StampInc) as the 'next best guess'
162  update_tail_stamp(stamp + StampInc);
163  }
164 
165  return wasTail;
166  }
167 
168  void add_to_global_retired_nodes(deletable_object_with_stamp* chunk) { add_to_global_retired_nodes(chunk, chunk); }
169 
170  void add_to_global_retired_nodes(deletable_object_with_stamp* first_chunk, deletable_object_with_stamp* last_chunk) {
171  assert(first_chunk != nullptr && last_chunk != nullptr);
172  auto* n = global_retired_nodes.load(std::memory_order_relaxed);
173  do {
174  last_chunk->next_chunk = n;
175  // (10) - this release-CAS synchronizes-with the acquire_xchg (11)
176  } while (!global_retired_nodes.compare_exchange_weak(
177  n, first_chunk, std::memory_order_release, std::memory_order_relaxed));
178  }
179 
180  deletable_object_with_stamp* steal_global_retired_nodes() {
181  if (global_retired_nodes.load(std::memory_order_relaxed) != nullptr) {
182  // (11) - this acquire-xchg synchronizes-with the release-CAS (10)
183  return global_retired_nodes.exchange(nullptr, std::memory_order_acquire);
184  }
185  return nullptr;
186  }
187 
188  stamp_t head_stamp() {
189  // (12) - this seq-cst-load enforces a total order with (2)
190  return head->stamp.load(std::memory_order_seq_cst);
191  }
192  stamp_t tail_stamp() {
193  // (13) - this acquire-load synchronizes-with the release-CAS (16)
194  return tail->stamp.load(std::memory_order_acquire);
195  }
196 
197  thread_control_block* acquire_control_block() { return global_thread_block_list.acquire_entry(); }
198 
199 private:
200  static const unsigned DeleteMark = 1;
201  static const unsigned TagInc = 2;
202  static const unsigned MarkMask = (1 << MarkBits) - 1;
203 
204  static marked_ptr make_marked(thread_control_block* p, const marked_ptr& mark) {
205  return marked_ptr(p, (mark.mark() + TagInc) & MarkMask);
206  }
207 
208  static marked_ptr make_clean_marked(thread_control_block* p, const concurrent_ptr& mark) {
209  auto m = mark.load(std::memory_order_relaxed);
210  return marked_ptr(p, (m.mark() + TagInc) & MarkMask & ~DeleteMark);
211  }
212 
213  void update_tail_stamp(size_t stamp) {
214  // In the best case the stamp of tail equals the stamp of tail's predecessor (in prev
215  // direction), but we don't want to waste too much time finding the "actual" predecessor.
216  // Therefore we simply check whether the block referenced by tail->next is the actual
217  // predecessor and if so take its stamp. Otherwise we simply use the stamp that was
218  // passed (which is kind of a "best guess").
219  // (14) - this acquire-load synchronizes-with the release-stores (8, 27)
220  auto last = tail->next.load(std::memory_order_acquire);
221  // (15) - this acquire-load synchronizes-with the release-stores (4, 5, 9, 21, 28)
222  auto last_prev = last->prev.load(std::memory_order_acquire);
223  auto last_stamp = last->stamp.load(std::memory_order_relaxed);
224  if (last_stamp > stamp && last_prev.get() == tail && tail->next.load(std::memory_order_relaxed) == last) {
225  assert((last_stamp & PendingPush) == 0);
226  assert((last_stamp & NotInList) == 0);
227  assert(last_stamp >= stamp);
228  if (last.get() != head) {
229  stamp = last_stamp;
230  } else {
231  // Special case when we take the stamp from head - the stamp in head gets incremented
232  // before a new block is actually inserted, but we must not use such a value if the block
233  // is not yet inserted. By updating prev with an incremented version a pending insertion
234  // would fail and cause a retry, therefore enforcing the strict odering.
235  // However, since we are potentially disturbing push operations, we only want to do this if
236  // it is "worth it", i.e., if the stamp we read from head is at least one increment ahead
237  // of our "next best guess".
238  if (stamp < last_stamp - StampInc && head->prev.compare_exchange_strong(last_prev,
239  make_marked(last_prev.get(), last_prev),
240  std::memory_order_relaxed)) {
241  stamp = last_stamp;
242  }
243  }
244  }
245 
246  // Try to update tail->stamp, but only as long as our new value is actually greater.
247  auto tail_stamp = tail->stamp.load(std::memory_order_relaxed);
248  while (tail_stamp < stamp) {
249  // (16) - this release-CAS synchronizes-with the acquire-load (13)
250  if (tail->stamp.compare_exchange_weak(tail_stamp, stamp, std::memory_order_release)) {
251  break;
252  }
253  }
254  }
255 
256  static bool remove_from_prev_list(marked_ptr& prev, marked_ptr b, marked_ptr& next) {
257  PERF_COUNTER(iterations, b->counters.remove_prev_iterations);
258 
259  const auto my_stamp = b->stamp.load(std::memory_order_relaxed);
260  marked_ptr last = nullptr;
261  for (;;) {
262  iterations.inc();
263 
264  // check if the block is already deleted
265  if (next.get() == prev.get()) {
266  next = b->next.load(std::memory_order_relaxed);
267  return false;
268  }
269 
270  auto prev_prev = prev->prev.load(std::memory_order_relaxed);
271  auto prev_stamp = prev->stamp.load(std::memory_order_relaxed);
272 
273  // check if prev has been removed
274  if (prev_stamp > my_stamp || // prev has been reinserted already
275  ((prev_stamp & NotInList) != 0)) // prev has been removed
276  {
277  return true;
278  }
279 
280  if ((prev_prev.mark() & DeleteMark) != 0) {
281  if (!mark_next(prev, prev_stamp)) {
282  // prev is marked for deletion, but mark_next failed because the stamp
283  // of prev has been updated - i.e., prev has been deleted already (and
284  // maybe even reinserted)
285  // -> this implies that b must have been removed as well.
286  return true;
287  }
288  // This acquire-reload is needed to establish a happens-before relation
289  // between the remove operations and the reclamation of a node.
290  // (17) - this acquire-load synchronizes-with the release-stores (4, 9, 21, 28)
291  prev = prev->prev.load(std::memory_order_acquire);
292  continue;
293  }
294 
295  // We need need to obtain a consistent set of "prev" and "stamp" values
296  // from next, otherwise we could wrongfully update next_prev's stamp in
297  // save_next_as_last_and_move_next_to_next_prev, since we cannot be sure
298  // if the "prev" value we see in the reload belongs to a block that is
299  // part of the list.
300 
301  // (18) - this acquire-load synchronizes-with the release-stores (4, 5, 9, 21, 28)
302  auto next_prev = next->prev.load(std::memory_order_acquire);
303  // (19) - this acquire-load synchronizes-with the release-stores (2, 3, 6)
304  auto next_stamp = next->stamp.load(std::memory_order_acquire);
305 
306  if (next_prev != next->prev.load(std::memory_order_relaxed)) {
307  continue;
308  }
309 
310  if (next_stamp < my_stamp) {
311  next = b->next.load(std::memory_order_relaxed);
312  return false;
313  }
314 
315  // Check if next has been removed from list or whether it is currently getting inserted.
316  // It could be that the block is already inserted, but the PendingPush flag has not yet
317  // been cleared. Unfortunately, there is no way to identify this case here, so we have
318  // to go back yet another block.
319  // We can help resetting this flag once we are sure that the block is already part of the
320  // list, which is exactly what happens in save_next_as_last_and_move_next_to_next_prev.
321  if ((next_stamp & (NotInList | PendingPush)) != 0) {
322  if (last.get() != nullptr) {
323  next = last;
324  last.reset();
325  } else {
326  // (20) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
327  next = next->next.load(std::memory_order_acquire);
328  }
329  continue;
330  }
331 
332  if (remove_or_skip_marked_block(next, last, next_prev, next_stamp)) {
333  continue;
334  }
335 
336  // check if next is the predecessor of b
337  if (next_prev.get() != b.get()) {
338  save_next_as_last_and_move_next_to_next_prev(next_prev, next, last);
339  continue;
340  }
341 
342  // unlink "b" from prev list
343  // (21) - this release-CAS synchronizes-with the acquire-loads (5, 9, 15, 17, 18, 22, 26)
344  if (next->prev.compare_exchange_strong(
345  next_prev, make_marked(prev.get(), next_prev), std::memory_order_release, std::memory_order_relaxed)) {
346  return false;
347  }
348 
349  // Back-Off
350  }
351  }
352 
353  static void remove_from_next_list(marked_ptr prev, marked_ptr removed, marked_ptr next) {
354  PERF_COUNTER(iterations, removed->counters.remove_next_iterations);
355 
356  const auto my_stamp = removed->stamp.load(std::memory_order_relaxed);
357 
358  marked_ptr last = nullptr;
359  for (;;) {
360  iterations.inc();
361 
362  // FIXME - why do we have to load next_prev before prev_next??
363  // We have to ensure that next is part of the list before obtaining the
364  // pointer we have to update, in order to ensure that we don't set the
365  // pointer to a block that has been removed in the meantime.
366 
367  // (22) - this acquire-load synchronizes-with the release-stores (4, 5, 9, 21, 28)
368  auto next_prev = next->prev.load(std::memory_order_acquire);
369  // (23) - this acquire-load synchronizes-with the release-stores (2, 3, 6)
370  auto next_stamp = next->stamp.load(std::memory_order_acquire);
371 
372  if (next_prev != next->prev.load(std::memory_order_relaxed)) {
373  continue;
374  }
375 
376  // check if next has been removed from list
377  if ((next_stamp & (NotInList | PendingPush)) != 0) {
378  if (last.get() != nullptr) {
379  next = last;
380  last.reset();
381  } else {
382  // (24) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
383  next = next->next.load(std::memory_order_acquire);
384  }
385  continue;
386  }
387 
388  // (25) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
389  auto prev_next = prev->next.load(std::memory_order_acquire);
390  auto prev_stamp = prev->stamp.load(std::memory_order_relaxed);
391 
392  assert(prev.get() != removed.get() || prev_stamp > my_stamp);
393 
394  // check if prev has a higher stamp than the block we want to remove.
395  if (prev_stamp > my_stamp || ((prev_stamp & NotInList) != 0)) {
396  // due to strict order of stamps the prev block must have been removed already - and with it b.
397  return;
398  }
399 
400  // check if prev block is marked for deletion
401  if ((prev_next.mark() & DeleteMark) != 0) {
402  // This acquire-load is needed to establish a happens-before relation
403  // between the different remove operations and the reclamation of a node.
404  // (26) - this acquire-load synchronizes-with the release-stores (4, 9, 21, 28)
405  prev = prev->prev.load(std::memory_order_acquire);
406  continue;
407  }
408 
409  if (next.get() == prev.get()) {
410  return;
411  }
412 
413  if (remove_or_skip_marked_block(next, last, next_prev, next_stamp)) {
414  continue;
415  }
416 
417  // check if next is the predecessor of prev
418  if (next_prev.get() != prev.get()) {
419  save_next_as_last_and_move_next_to_next_prev(next_prev, next, last);
420  continue;
421  }
422 
423  if (next_stamp <= my_stamp || prev_next.get() == next.get()) {
424  return;
425  }
426 
427  auto new_next = make_marked(next.get(), prev_next);
428  if (next->prev.load(std::memory_order_relaxed) == next_prev &&
429  // (27) - this release-CAS synchronizes-with the acquire-loads (7, 8, 14, 20, 24, 25, 29, 31, 32)
430  prev->next.compare_exchange_weak(prev_next, new_next, std::memory_order_release, std::memory_order_relaxed)) {
431  if ((next->next.load(std::memory_order_relaxed).mark() & DeleteMark) == 0) {
432  return;
433  }
434  }
435  // Back-Off
436  }
437  }
438 
439  static bool
440  remove_or_skip_marked_block(marked_ptr& next, marked_ptr& last, marked_ptr next_prev, stamp_t next_stamp) {
441  // check if next is marked
442  if ((next_prev.mark() & DeleteMark) != 0) {
443  if (last.get() != nullptr) {
444  // check if next has "overtaken" last
445  assert((next.mark() & DeleteMark) == 0);
446  if (mark_next(next, next_stamp) && last->prev.load(std::memory_order_relaxed) == next) {
447  // unlink next from prev-list
448  // (28) - this release-CAS synchronizes-with the acquire-loads (5, 9, 15, 17, 18, 22, 26)
449  last->prev.compare_exchange_strong(
450  next, make_marked(next_prev.get(), next), std::memory_order_release, std::memory_order_relaxed);
451  }
452  next = last;
453  last.reset();
454  } else {
455  // (29) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
456  next = next->next.load(std::memory_order_acquire);
457  }
458 
459  return true;
460  }
461  return false;
462  }
463 
464  static void save_next_as_last_and_move_next_to_next_prev(marked_ptr next_prev, marked_ptr& next, marked_ptr& last) {
465  // (30) - this acquire-load synchronizes-with the release-stores (3, 6)
466  size_t next_prev_stamp = next_prev->stamp.load(std::memory_order_acquire);
467 
468  if (((next_prev_stamp & PendingPush) != 0) && next_prev == next->prev.load(std::memory_order_relaxed)) {
469  assert((next_prev_stamp & NotInList) == 0);
470  // since we got here via an (unmarked) prev pointer next_prev has been added to
471  // the "prev-list", but the PendingPush flag has not been cleared yet.
472  // i.e., the push operation for next_prev is still pending -> help clear the PendingPush flag
473  auto expected = next_prev_stamp;
474  const auto new_stamp = next_prev_stamp + (StampInc - PendingPush);
475  if (!next_prev->stamp.compare_exchange_strong(expected, new_stamp, std::memory_order_relaxed)) {
476  // CAS operation failed, i.e., the stamp of next_prev has been changed
477  // since we read it. Check if some other thread cleared the flag already
478  // or whether next_prev has been removed (and potentially readded).
479  if (expected != new_stamp) {
480  // the stamp has been updated to an unexpected value, so next_prev has been removed
481  // already -> we cannot move to next_prev, but we can keep the current next and last.
482  return;
483  }
484  }
485  }
486  last = next;
487  next = next_prev;
488  }
489 
490  static marked_ptr set_mark_flag(concurrent_ptr& ptr, std::memory_order order) {
491  auto link = ptr.load(std::memory_order_relaxed);
492  for (;;) {
493  if (((link.mark() & DeleteMark) != 0) ||
494  ptr.compare_exchange_weak(
495  link, marked_ptr(link.get(), link.mark() | DeleteMark), order, std::memory_order_relaxed)) {
496  return link;
497  }
498  }
499  }
500 
501  // tries to mark the next ptr of 'block', as long as block's stamp matches the given stamp
502  static bool mark_next(marked_ptr block, size_t stamp) {
503  assert((stamp & (NotInList | PendingPush)) == 0);
504  // (31) - this acquire-load synchronizes-with the release-stores (1, 8, 27)
505  auto link = block->next.load(std::memory_order_acquire);
506  // We need acquire to synchronize-with the release store in push. This way it is
507  // guaranteed that the following stamp.load sees the NotInList flag or some newer
508  // stamp, thus causing termination of the loop.
509  while (block->stamp.load(std::memory_order_relaxed) == stamp) {
510  auto mark = link.mark();
511  if (((mark & DeleteMark) != 0) ||
512  // (32) - this acquire-reload synchronizes-with the release-stores (1, 8, 27)
513  block->next.compare_exchange_weak(
514  link, marked_ptr(link.get(), mark | DeleteMark), std::memory_order_relaxed, std::memory_order_acquire)) {
515  // Note: the C++11 standard states that "the failure argument shall be no stronger than
516  // the success argument". However, this has been relaxed in C++17.
517  // (see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0418r1.html)
518  // Some implementations (e.g., the Microsoft STL) perform runtime checks to enforce these
519  // requirements. So in case the above CAS operation causes runtime errors, one has to use
520  // release instead of relaxed order.
521  return true;
522  }
523  }
524  return false;
525  }
526 
527  thread_control_block* head;
528  thread_control_block* tail;
529 
530  alignas(64) std::atomic<deletable_object_with_stamp*> global_retired_nodes{nullptr};
531  alignas(64) detail::thread_block_list<thread_control_block> global_thread_block_list{};
532  friend class stamp_it;
533 };
534 
535 struct stamp_it::thread_data {
536  ~thread_data() {
537  assert(region_entries == 0);
538  if (control_block == nullptr) {
539  return;
540  }
541 
542  control_block->abandon();
543  control_block = nullptr;
544 
545  // reclaim as much nodes as possible
546  process_local_nodes();
547  if (first_retired_node != nullptr) {
548  // we still have retired nodes that cannot yet be reclaimed
549  // -> add them to the global list.
550  queue.add_to_global_retired_nodes(first_retired_node);
551  }
552  }
553 
554  void enter_region() {
555  if (++region_entries == 1) {
556  ensure_has_control_block();
557  queue.push(control_block);
558  }
559  }
560 
561  void leave_region() {
562  if (--region_entries == 0) {
563  auto wasLast = queue.remove(control_block);
564 
565  if (wasLast) {
566  process_global_nodes();
567  } else {
568  process_local_nodes();
569  if (number_of_retired_nodes > max_remaining_retired_nodes) {
570  queue.add_to_global_retired_nodes(first_retired_node);
571  first_retired_node = nullptr;
572  prev_retired_node = &first_retired_node;
573  number_of_retired_nodes = 0;
574  }
575  }
576  }
577  }
578 
579  void add_retired_node(deletable_object_with_stamp* p) {
580  p->stamp = queue.head_stamp();
581  *prev_retired_node = p;
582  prev_retired_node = &p->next;
583 
584  ++number_of_retired_nodes;
585  if (number_of_retired_nodes > try_reclaim_threshold) {
586  process_local_nodes();
587  }
588  }
589 
590 private:
591  void ensure_has_control_block() {
592  if (control_block == nullptr) {
593  control_block = queue.acquire_control_block();
594 #ifdef WITH_PERF_COUNTER
595  control_block->counters = performance_counters{}; // reset counters
596 #endif
597  }
598  }
599 
600  void process_local_nodes() {
601  auto tail_stamp = queue.tail_stamp();
602  std::size_t cnt = 0;
603  auto* cur = first_retired_node;
604  for (deletable_object_with_stamp* next = nullptr; cur != nullptr; cur = next) {
605  next = cur->next;
606  if (cur->stamp <= tail_stamp) {
607  cur->delete_self();
608  ++cnt;
609  } else {
610  break;
611  }
612  }
613 
614  first_retired_node = cur;
615  if (cur == nullptr) {
616  prev_retired_node = &first_retired_node;
617  }
618  number_of_retired_nodes -= cnt;
619  }
620 
621  void process_global_nodes() {
622  auto tail_stamp = queue.tail_stamp();
623  auto* cur_chunk = queue.steal_global_retired_nodes();
624 
625  if (first_retired_node != nullptr) {
626  first_retired_node->next_chunk = cur_chunk;
627  cur_chunk = first_retired_node;
628  first_retired_node = nullptr;
629  prev_retired_node = &first_retired_node;
630  number_of_retired_nodes = 0;
631  }
632  if (cur_chunk == nullptr) {
633  return;
634  }
635 
636  stamp_t lowest_stamp;
637  auto process_chunk_nodes = [&tail_stamp, &lowest_stamp](deletable_object_with_stamp* chunk) {
638  auto* cur = chunk;
639  while (cur != nullptr) {
640  if (cur->stamp <= tail_stamp) {
641  lowest_stamp = std::min(lowest_stamp, cur->stamp);
642  auto* next = cur->next;
643  cur->delete_self();
644  cur = next;
645  } else {
646  break; // we cannot process any more nodes from this chunk
647  }
648  }
649  return cur;
650  };
651 
652  restart:
653  lowest_stamp = std::numeric_limits<stamp_t>::max();
654  deletable_object_with_stamp* first_remaining_chunk = nullptr;
655  deletable_object_with_stamp* last_remaining_chunk = nullptr;
656  deletable_object_with_stamp** prev_remaining_chunk = &first_remaining_chunk;
657  while (cur_chunk != nullptr) {
658  auto* next_chunk = cur_chunk->next_chunk;
659  auto* remaining_chunk = process_chunk_nodes(cur_chunk);
660  if (remaining_chunk != nullptr) {
661  *prev_remaining_chunk = remaining_chunk;
662  last_remaining_chunk = remaining_chunk;
663  prev_remaining_chunk = &remaining_chunk->next_chunk;
664  }
665  cur_chunk = next_chunk;
666  }
667 
668  *prev_remaining_chunk = nullptr;
669  if (first_remaining_chunk != nullptr) {
670  auto new_tail_stamp = queue.tail_stamp();
671  if (lowest_stamp < new_tail_stamp) {
672  cur_chunk = first_remaining_chunk;
673  tail_stamp = new_tail_stamp;
674  goto restart;
675  } else {
676  assert(last_remaining_chunk != nullptr);
677  queue.add_to_global_retired_nodes(first_remaining_chunk, last_remaining_chunk);
678  }
679  }
680  }
681 
682  // This threshold defines the max. number of nodes a thread may collect
683  // in the local retire-list before trying to reclaim them. It is checked
684  // every time a new node is added to the local retire-list.
685  static const std::size_t try_reclaim_threshold = 40;
686  // The max. number of nodes that may remain a threads local retire-list
687  // when it leaves it's critical region. If there are more nodes in the
688  // list, then the whole list will be added to the global retire-list.
689  static const std::size_t max_remaining_retired_nodes = 20;
690 
691  thread_control_block* control_block = nullptr;
692  unsigned region_entries = 0;
693  std::size_t number_of_retired_nodes = 0;
694 
695  deletable_object_with_stamp* first_retired_node = nullptr;
696  deletable_object_with_stamp** prev_retired_node = &first_retired_node;
697 
698  friend class stamp_it;
699  ALLOCATION_COUNTER(stamp_it);
700 };
701 
702 inline stamp_it::region_guard::region_guard() noexcept {
703  local_thread_data().enter_region();
704 }
705 
706 inline stamp_it::region_guard::~region_guard() // noexcept ??
707 {
708  local_thread_data().leave_region();
709 }
710 
711 template <class T, class MarkedPtr>
712 stamp_it::guard_ptr<T, MarkedPtr>::guard_ptr(const MarkedPtr& p) noexcept : base(p) {
713  if (this->ptr) {
714  local_thread_data().enter_region();
715  }
716 }
717 
718 template <class T, class MarkedPtr>
719 stamp_it::guard_ptr<T, MarkedPtr>::guard_ptr(const guard_ptr& p) noexcept : guard_ptr(MarkedPtr(p)) {}
720 
721 template <class T, class MarkedPtr>
722 stamp_it::guard_ptr<T, MarkedPtr>::guard_ptr(guard_ptr&& p) noexcept : base(p.ptr) {
723  p.ptr.reset();
724 }
725 
726 template <class T, class MarkedPtr>
727 typename stamp_it::guard_ptr<T, MarkedPtr>& stamp_it::guard_ptr<T, MarkedPtr>::operator=(const guard_ptr& p) noexcept {
728  if (&p == this) {
729  return *this;
730  }
731 
732  reset();
733  this->ptr = p.ptr;
734  if (this->ptr) {
735  local_thread_data().enter_region();
736  }
737 
738  return *this;
739 }
740 
741 template <class T, class MarkedPtr>
742 typename stamp_it::guard_ptr<T, MarkedPtr>& stamp_it::guard_ptr<T, MarkedPtr>::operator=(guard_ptr&& p) noexcept {
743  if (&p == this) {
744  return *this;
745  }
746 
747  reset();
748  this->ptr = std::move(p.ptr);
749  p.ptr.reset();
750 
751  return *this;
752 }
753 
754 template <class T, class MarkedPtr>
755 void stamp_it::guard_ptr<T, MarkedPtr>::acquire(const concurrent_ptr<T>& p, std::memory_order order) noexcept {
756  if (p.load(std::memory_order_relaxed) == nullptr) {
757  reset();
758  return;
759  }
760 
761  if (!this->ptr) {
762  local_thread_data().enter_region();
763  }
764  this->ptr = p.load(order);
765  if (!this->ptr) {
766  local_thread_data().leave_region();
767  }
768 }
769 
770 template <class T, class MarkedPtr>
771 bool stamp_it::guard_ptr<T, MarkedPtr>::acquire_if_equal(const concurrent_ptr<T>& p,
772  const MarkedPtr& expected,
773  std::memory_order order) noexcept {
774  auto actual = p.load(std::memory_order_relaxed);
775  if (actual == nullptr || actual != expected) {
776  reset();
777  return actual == expected;
778  }
779 
780  if (!this->ptr) {
781  local_thread_data().enter_region();
782  }
783  this->ptr = p.load(order);
784  if (!this->ptr || this->ptr != expected) {
785  local_thread_data().leave_region();
786  this->ptr.reset();
787  }
788 
789  return this->ptr == expected;
790 }
791 
792 template <class T, class MarkedPtr>
793 void stamp_it::guard_ptr<T, MarkedPtr>::reset() noexcept {
794  if (this->ptr) {
795  local_thread_data().leave_region();
796  }
797  this->ptr.reset();
798 }
799 
800 template <class T, class MarkedPtr>
801 void stamp_it::guard_ptr<T, MarkedPtr>::reclaim(Deleter d) noexcept {
802  this->ptr->set_deleter(std::move(d));
803  local_thread_data().add_retired_node(this->ptr.get());
804  reset();
805 }
806 
807 inline stamp_it::thread_data& stamp_it::local_thread_data() {
808  // workaround for a GCC issue with multiple definitions of __tls_guard
809  static thread_local thread_data local_thread_data;
810  return local_thread_data;
811 }
812 
813 #if _MSC_VER
814 __declspec(selectany) stamp_it::thread_order_queue stamp_it::queue;
815 #else
816 inline stamp_it::thread_order_queue stamp_it::queue;
817 #endif
818 
819 #ifdef WITH_PERF_COUNTER
820 inline stamp_it::performance_counters stamp_it::get_performance_counters() {
821  performance_counters result{};
822  std::for_each(
823  queue.global_thread_block_list.begin(), queue.global_thread_block_list.end(), [&result](const auto& block) {
824  result.push_calls += block.counters.push_calls;
825  result.push_iterations += block.counters.push_iterations;
826  result.remove_calls += block.counters.remove_calls;
827  result.remove_prev_iterations += block.counters.remove_prev_iterations;
828  result.remove_next_iterations += block.counters.remove_next_iterations;
829  });
830 
831  return result;
832 }
833 #endif
834 
835 #ifdef TRACK_ALLOCATIONS
836 inline void stamp_it::count_allocation() {
837  local_thread_data().allocation_counter.count_allocation();
838 }
839 
840 inline void stamp_it::count_reclamation() {
841  local_thread_data().allocation_counter.count_reclamation();
842 }
843 #endif
844 } // namespace xenium::reclamation
845 
846 #ifdef _MSC_VER
847  #pragma warning(pop)
848 #endif