Search code examples
c++atomicmemory-barriersmemory-modelstdatomic

Can relaxed memory order be used to observe a condition?


std::atomic<bool> b;

void f()
{
    // block A
    if(b.load(std::memory_order_relaxed))
    {
        // block B
    }
    // block C
}

void g()
{
    // block B
    b.store(true, std::memory_order_release);
}

Theoretically block B should only be executed if the atomic load returns true,
but is it possible that part of block B can get reordered before the load? store with release memory order guarantees that all operations on block B are a visible side effect, but does that still apply if the load is a relaxed operation?


Solution

  • You have two block Bs in your example. I'm talking about the one in the void f() load function.

    is it possible that part of block B can get reordered before the load?

    Yes. The compiler could hoist loads out of the if() body and do them before the b.load. This is likely to happen if both block B and C read the same non-atomic variable.

    And there are real-life mechanisms that will create this reordering even without compile-time reordering:

    Specifically, branch speculation (i.e. branch prediction + out-of-order speculative execution) will let the CPU start executing block B before the b.load() even starts.

    You can't depend on "causality" or any kind of reasoning like "it would have to know the b.load() result before it can know what to execute next".

    Or a compiler could potentially do if-conversion of the if() into branchless code, if there aren't any stores in block B. Then it could pretty obviously reorder with non-atomic loads, or other relaxed or acquire loads, that were in block B and C.

    (Remember acq/rel are one-way barriers.)


    Reasoning like this (based on what real compilers and CPUs can do) can be useful to prove that something isn't safe. But be careful of going the other way: reasoning based on "safe on the compiler I know about" doesn't always mean "safe in portable ISO C++".

    Sometimes "safe on the compiler I know about" is more or less sufficient, but it's hard to separate that from "happens to work on the compile I know about", where a future compiler version or a seemingly unrelated source change could break something.

    So always try to reason about memory ordering in terms of the C++ memory model, as well as in terms of how it can compile efficiently for an ISA you care about (e.g. strongly-ordered x86). Like you might see that relaxed would allow a compile-time reordering that's actually useful in your case.