• Relacy verification: Asymmetric SMR / Hazard Pointer with mo_consume an

    From Chris M. Thomasson@3:633/10 to All on Tue Oct 13 16:12:12 2026
    Subject: Relacy verification: Asymmetric SMR / Hazard Pointer with mo_consume and zero seq_cst...

    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.

    Reader Fast Path: Uses pure relaxed pointer loads and hazard stores,
    combined with a memory_order_consume validation load (node1 == node0) to cleanly chain the data dependency without heavy ordering bottlenecks.

    Writer Exchanges: Uses mo_acq_rel instead of sequential consistency.

    Poller Mechanism: (ah-hoc) Handles cleanup safely via an exclusive mutex (m_poll), a central deferred queue (m_defer0), a two-pass hazard scan (prv_poll), and a rl::systemwide_fence($) between passes to prevent any use-after-free conditions. It just cleared a rigorous simulation run
    of 500,000 iterations cleanly across 5 readers and 3 writers with zero
    data races, leaks, or race conditions

    I attached my code. Can you run it on your end? Any thoughts? Thanks everybody!

    struct ct::smr_test::smr_test_v000
    13% (65536/500000)
    26% (131072/500000)
    39% (196608/500000)
    52% (262144/500000)
    65% (327680/500000)
    78% (393216/500000)
    91% (458752/500000)
    iterations: 500000
    total time: 14093
    throughput: 35478


    D:\ct_dev\ct_msvc\projects\ct_relacy_test\x64\Release\ct_relacy_test.exe (process 2408) exited with code 0 (0x0).
    Press any key to close this window . . .

    my experimental test code:


    ct_smr_test.hpp
    ______________________
    #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 = 8;
    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_relaxed, $);
    }

    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!
    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);
    break;
    }

    m_threads[tidx].m_haz_0.store(node0,
    rl::mo_relaxed, $);

    // actually, iirc, a single seq_cst right here
    should work?

    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_relaxed, $);
    }
    };

    // 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)
    {
    unsigned long data = VAR(node->m_data);
    RL_ASSERT(data == 123 || data == 456);

    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);
    }
    }
    }
    }
    ________________________


    ct_main.hpp
    ________________________
    #include <relacy/relacy.hpp>


    // tests
    #include "ct_smr_test.hpp"
    #include "ct_multex_test.hpp"


    #include <cstdio>
    #include <cstddef>
    #include <cstdlib>
    #include <string>
    #include <vector>


    int main(int argc, char** argv)
    {
    {
    rl::test_params p;

    p.iteration_count = argc > 1 ? (unsigned)atoi(argv[1]) : 500000;
    p.execution_depth_limit = 100000;

    if (argc > 2) {
    std::string m = argv[2];
    if (m == "full") p.search_type = rl::sched_full;
    else if (m == "bound") { p.search_type = rl::sched_bound; p.context_bound = argc > 3 ? atoi(argv[3]) : 2; }
    }

    {
    //ct::multex_test::test::manifest(p);
    ct::smr_test::test::manifest(p);
    }
    }

    return 0;
    }



    /*
    #include <relacy/relacy.hpp>
    #include <iostream>


    int
    main()
    {
    std::cout << "ct_relacy_test\n";
    std::cout << "____________________________________________" <<
    std::endl;

    {

    }

    std::cout << "____________________________________________" <<
    std::endl;
    std::cout << "ct_relacy_test fin! :^)\n";

    return 0;
    }

    */
    ________________________


    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Chris M. Thomasson@3:633/10 to All on Tue Oct 13 18:19:23 2026
    Subject: Re: Relacy verification: Asymmetric SMR / Hazard Pointer with mo_consume and zero seq_cst...

    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.

    Reader Fast Path: Uses pure relaxed pointer loads and hazard stores, combined with a memory_order_consume validation load (node1 == node0) to cleanly chain the data dependency without heavy ordering bottlenecks.

    Writer Exchanges: Uses mo_acq_rel instead of sequential consistency.

    Poller Mechanism: (ah-hoc) Handles cleanup safely via an exclusive mutex (m_poll), a central deferred queue (m_defer0), a two-pass hazard scan (prv_poll), and a rl::systemwide_fence($) between passes to prevent any use-after-free conditions.ÿÿ It just cleared a rigorous simulation run
    of 500,000 iterations cleanly across 5 readers and 3 writers with zero
    data races, leaks, or race conditions

    I attached my code. Can you run it on your end? Any thoughts? Thanks everybody!

    struct ct::smr_test::smr_test_v000
    13% (65536/500000)
    26% (131072/500000)
    39% (196608/500000)
    52% (262144/500000)
    65% (327680/500000)
    78% (393216/500000)
    91% (458752/500000)
    iterations: 500000
    total time: 14093
    throughput: 35478


    D:\ct_dev\ct_msvc\projects\ct_relacy_test\x64\Release\ct_relacy_test.exe (process 2408) exited with code 0 (0x0).
    Press any key to close this window . . .

    my experimental test code:


    ct_smr_test.hpp
    ______________________
    #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 = 8;
    ÿÿÿÿÿÿÿ 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_relaxed, $);
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ }

    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 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!
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 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);
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ break;
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ }

    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    Okay. I need to clarify. This path should NEVER be hit wrt my current
    testing regime. BUT, it should null out the per thread hazard pointer
    anyway.





    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ m_threads[tidx].m_haz_0.store(node0,
    rl::mo_relaxed, $);

    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ // actually, iirc, a single seq_cst right here
    should work?

    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 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_relaxed, $);
    ÿÿÿÿÿÿÿÿÿÿÿ }
    ÿÿÿÿÿÿÿ };

    ÿÿÿÿÿÿÿ // 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)
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ {
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ unsigned long data = VAR(node->m_data);
    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ RL_ASSERT(data == 123 || data == 456);

    ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 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);
    ÿÿÿÿÿÿÿÿÿÿÿ }
    ÿÿÿÿÿÿÿ }
    ÿÿÿ }
    }
    ________________________


    ct_main.hpp
    ________________________
    #include <relacy/relacy.hpp>


    // tests
    #include "ct_smr_test.hpp"
    #include "ct_multex_test.hpp"


    #include <cstdio>
    #include <cstddef>
    #include <cstdlib>
    #include <string>
    #include <vector>


    int main(int argc, char** argv)
    {
    ÿÿÿ {
    ÿÿÿÿÿÿÿ rl::test_params p;

    ÿÿÿÿÿÿÿ p.iteration_count = argc > 1 ? (unsigned)atoi(argv[1]) : 500000;
    ÿÿÿÿÿÿÿ p.execution_depth_limit = 100000;

    ÿÿÿÿÿÿÿ if (argc > 2) {
    ÿÿÿÿÿÿÿÿÿÿÿ std::string m = argv[2];
    ÿÿÿÿÿÿÿÿÿÿÿ if (m == "full") p.search_type = rl::sched_full;
    ÿÿÿÿÿÿÿÿÿÿÿ else if (m == "bound") { p.search_type = rl::sched_bound; p.context_bound = argc > 3 ? atoi(argv[3]) : 2; }
    ÿÿÿÿÿÿÿ }

    ÿÿÿÿÿÿÿ {
    ÿÿÿÿÿÿÿÿÿÿÿ //ct::multex_test::test::manifest(p);
    ÿÿÿÿÿÿÿÿÿÿÿ ct::smr_test::test::manifest(p);
    ÿÿÿÿÿÿÿ }
    ÿÿÿ }

    ÿÿÿ return 0;
    }



    /*
    #include <relacy/relacy.hpp>
    #include <iostream>


    int
    main()
    {
    ÿÿÿ std::cout << "ct_relacy_test\n";
    ÿÿÿ std::cout << "____________________________________________" << std::endl;

    ÿÿÿ {

    ÿÿÿ }

    ÿÿÿ std::cout << "____________________________________________" << std::endl;
    ÿÿÿ std::cout << "ct_relacy_test fin! :^)\n";

    ÿÿÿ return 0;
    }

    */
    ________________________



    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Chris M. Thomasson@3:633/10 to All on Wed Oct 14 14:14:56 2026
    Subject: Re: Relacy verification: Asymmetric SMR / Hazard Pointer with mo_consume and zero seq_cst...

    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: ____________________
    #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_relaxed, $);
    }

    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!
    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
    ) {
    per_node* node0 = m_node.load(rl::mo_relaxed, $);
    m_threads[tidx].m_haz_0.store(node0, rl::mo_relaxed, $);
    return node0;
    }


    void
    smr_unload(
    unsigned long tidx
    ) {
    // humm...
    m_threads[tidx].m_haz_0.store(nullptr, rl::mo_relaxed, $);
    }
    };

    // 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)
    {
    unsigned long data = VAR(node->m_data);
    RL_ASSERT(data == 123 || data == 456);

    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);
    }
    }
    }
    }
    ____________________

    --- PyGate Linux v1.5.19
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Chris M. Thomasson@3:633/10 to All on Thu Oct 15 11:12:52 2026
    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)