Subject: Re: Relacy verification: Asymmetric SMR / Hazard Pointer with mo_consume and zero seq_cst...
On 10/14/2026 2:14 PM, Chris M. Thomasson wrote:
On 10/13/2026 4:12 PM, Chris M. Thomasson wrote:
Just wanted to share a quick update on a Safe Memory Reclamation (SMR)
hazard pointer engine I've been putting through its paces using
Relacy. I managed to design an asymmetric, poller-driven deferred
reclamation model that successfully eliminates heavy mo_seq_cst
operations entirely from the runtime path.[...]
Oh my... I don't even need to use the SMR loop on smr_load! This works
fine. All relaxed, no consume for the smr_load. Here is a relacy test unit:[...]
Well, that was totally wrong!!!! Its an error my part, god damn it! I
was running another test and thought it was my smr test; thought it
passed, and prematurely posted it. Stupid ME! Relacy bites the dust and reports data races. Uggg!!!
So, I talked to some people. Apparently, I also cannot get away with
using relaxed for smr_unload. So, this is finally async SMR that works
well with the Relacy!
Also, notice in prv_poll(), I need to use an acquire after the first
pass and after the async systemwide_fence.
_____________________________
#pragma once
#include <relacy/relacy.hpp>
#include <cstdio>
#include <cstddef>
#include <cstdlib>
#include <array>
#include <string>
#include <vector>
namespace ct
{
namespace smr_test
{
constexpr unsigned long CT_THREADS_READERS = 5;
constexpr unsigned long CT_THREADS_WRITERS = 3;
constexpr unsigned long CT_THREADS = CT_THREADS_READERS + CT_THREADS_WRITERS;
constexpr unsigned long CT_THREADS_READER_ITER_N = 12;
constexpr unsigned long CT_THREADS_WRITER_ITER_N = 8;
struct per_node
{
VAR_T(per_node*) m_defer = { nullptr };
VAR_T(unsigned long) m_data = { 0 };
};
struct per_thread
{
rl::atomic<per_node*> m_haz_0 = { nullptr };
};
struct poller
{
rl::mutex m_poll;
VAR_T(per_node*) m_defer0 = nullptr;
void
prv_poll(
per_node*& defer,
std::array<per_thread, CT_THREADS>& threads
) {
per_node* head = defer;
defer = nullptr; // Take ownership of the caller's
local batch
// Merge any previously deferred nodes from m_defer0
into the processing batch
per_node* curr = VAR(m_defer0);
VAR(m_defer0) = nullptr;
while (curr)
{
per_node* next_node = VAR(curr->m_defer);
VAR(curr->m_defer) = head;
head = curr;
curr = next_node;
}
// Two-Pass Hazard Pointer Scan:
// Pass 1: Capture all thread hazard slots
std::vector<per_node*> haz_pass1(threads.size());
for (size_t i = 0; i < threads.size(); ++i)
{
haz_pass1[i] =
threads[i].m_haz_0.load(rl::mo_relaxed, $);
}
// Synchronization barrier between scans
rl::systemwide_fence($);
// Pass 2: Capture all thread hazard slots again
std::vector<per_node*> haz_pass2(threads.size());
for (size_t i = 0; i < threads.size(); ++i)
{
haz_pass2[i] =
threads[i].m_haz_0.load(rl::mo_acquire, $);
}
per_node* remaining = nullptr;
while (head)
{
per_node* next = VAR(head->m_defer);
bool protected_node = false;
// Check if the node is protected in either pass
snapshot
for (auto haz : haz_pass1)
{
if (haz == head)
{
protected_node = true;
break;
}
}
if (! protected_node)
{
for (auto haz : haz_pass2)
{
if (haz == head)
{
protected_node = true;
break;
}
}
}
if (protected_node)
{
// Keep it in the deferred list
VAR(head->m_defer) = remaining;
remaining = head;
}
else
{
// Safe to reclaim!
VAR(head->m_data) = 0xDEADBEEF;
delete head;
}
head = next;
}
// Store any still-protected nodes back into the
central poller queue
VAR(m_defer0) = remaining;
}
bool
try_poll(
per_node*& defer,
std::array<per_thread, CT_THREADS>& threads
) {
if (m_poll.try_lock($))
{
prv_poll(defer, threads);
m_poll.unlock($);
return true;
}
return false;
}
void
poll(
per_node*& defer,
std::array<per_thread, CT_THREADS>& threads
) {
m_poll.lock($);
prv_poll(defer, threads);
m_poll.unlock($);
}
};
struct state
{
std::array<per_thread, CT_THREADS> m_threads;
poller m_poller = { };
rl::atomic<per_node*> m_node = { };
// relacy hooks, so to speak... ;^o
// relaxed because everything either has not
// started yet, or everything is already done...
void
before()
{
per_node* node = new per_node();
VAR(node->m_data) = 456;
VAR(node->m_defer) = nullptr;
m_node.store(node, rl::mo_relaxed, $);
}
void
after()
{
per_node* node = m_node.load(rl::mo_relaxed, $);
if (node)
{
delete node;
}
// Also clean up any lingering nodes left in the
poller's m_defer0 at teardown
per_node* poll_defer = VAR(m_poller.m_defer0);
while (poll_defer)
{
per_node* next = VAR(poll_defer->m_defer);
delete poll_defer;
poll_defer = next;
}
}
per_node*
smr_load(
unsigned long tidx
) {
for (;;)
{
per_node* node0 = m_node.load(rl::mo_relaxed, $);
if (! node0)
{
RL_ASSERT(0); // hould never happen in this test!
m_threads[tidx].m_haz_0.store(nullptr, rl::mo_relaxed, $);
break;
}
m_threads[tidx].m_haz_0.store(node0,
rl::mo_relaxed, $);
// change this to relaxed and it fails...
per_node* node1 = m_node.load(rl::mo_consume, $);
if (node0 == node1)
{
return node0;
}
}
return nullptr;
}
void
smr_unload(
unsigned long tidx
) {
// humm...
m_threads[tidx].m_haz_0.store(nullptr, rl::mo_release, $);
}
};
// the actual test unit...
struct smr_test_v000
: rl::test_suite<smr_test_v000, CT_THREADS>
{
state m_state = { };
void before()
{
m_state.before();
}
void after()
{
m_state.after();
}
void
thread_reader(
unsigned int tidx
) {
for (unsigned long i = 0; i < CT_THREADS_READER_ITER_N;
++i)
{
per_node* node = m_state.smr_load(tidx);
if (node)
{
// dummy load to poke Relacy... ;^)
(void)m_state.m_threads[tidx].m_haz_0.load(rl::mo_relaxed, $);
unsigned long data = VAR(node->m_data);
RL_ASSERT((data == 123 || data == 456) && (data
!= 0xDEADBEEF));
m_state.smr_unload(tidx);
}
}
}
void
thread_writer(
unsigned int tidx
) {
per_node* my_defer = nullptr;
for (unsigned long i = 0; i < CT_THREADS_WRITER_ITER_N;
++i)
{
per_node* node = new per_node();
VAR(node->m_data) = 456;
VAR(node->m_defer) = nullptr;
per_node* old = m_state.m_node.exchange(node, rl::mo_acq_rel, $);
if (old)
{
VAR(old->m_defer) = my_defer;
my_defer = old;
}
// Opportunistically try to poll and sweep nodes
m_state.m_poller.try_poll(my_defer, m_state.m_threads);
}
// Drain remaining deferred nodes upon thread exit
m_state.m_poller.poll(my_defer, m_state.m_threads);
}
void
thread(
unsigned int tidx
) {
if (tidx < CT_THREADS_READERS)
{
thread_reader(tidx);
}
else
{
thread_writer(tidx);
}
}
};
}
}
namespace ct
{
namespace smr_test
{
namespace test
{
inline void
manifest(
rl::test_params& p
) {
rl::simulate<ct::smr_test::smr_test_v000>(p);
}
}
}
}
_________________________
struct ct::smr_test::smr_test_v000
2% (65536/3000000)
4% (131072/3000000)
6% (196608/3000000)
8% (262144/3000000)
10% (327680/3000000)
13% (393216/3000000)
15% (458752/3000000)
17% (524288/3000000)
19% (589824/3000000)
21% (655360/3000000)
24% (720896/3000000)
26% (786432/3000000)
28% (851968/3000000)
30% (917504/3000000)
32% (983040/3000000)
34% (1048576/3000000)
37% (1114112/3000000)
39% (1179648/3000000)
41% (1245184/3000000)
43% (1310720/3000000)
45% (1376256/3000000)
48% (1441792/3000000)
50% (1507328/3000000)
52% (1572864/3000000)
54% (1638400/3000000)
56% (1703936/3000000)
58% (1769472/3000000)
61% (1835008/3000000)
63% (1900544/3000000)
65% (1966080/3000000)
67% (2031616/3000000)
69% (2097152/3000000)
72% (2162688/3000000)
74% (2228224/3000000)
76% (2293760/3000000)
78% (2359296/3000000)
80% (2424832/3000000)
83% (2490368/3000000)
85% (2555904/3000000)
87% (2621440/3000000)
89% (2686976/3000000)
91% (2752512/3000000)
93% (2818048/3000000)
96% (2883584/3000000)
98% (2949120/3000000)
iterations: 3000000
total time: 119875
throughput: 25026
D:\ct_dev\ct_msvc\projects\ct_relacy_test\x64\Release\ct_relacy_test.exe (process 17000) exited with code 0 (0x0).
Press any key to close this window . . .
--- PyGate Linux v1.5.19
* Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)