Question Details

No question body available.

Tags

c++ multithreading cpu-architecture atomic arm64

Answers (1)

Accepted Answer Available
Accepted Answer
June 26, 2026 Score: 5 Rep: 381,592 Quality: Expert Completeness: 80%

This time your if condition is r2>0 which is only true if t2's x.load sees t1's x.store, so no amount of local value-range analysis can let t2's y.store run independently of its x.load if condition. (Your previous question had a >=0, allowing the conditional store to actually become unconditional because the condition would be true for either possible load result. This was the key difference.)

I think even in the C++ abstract machine, I'd call r1==2 an out-of-thin-air value which is disallowed by that rule. If not, it's the closest you can come to OoTA without actually crossing the line. It's maybe not exactly circular, but it can only happen as part of inter-thread speculation allowing speculation to confirm itself. (Speculating that r2>0 and letting y.store(2) become visible to other threads before t1's load has executed. Or value-prediction for t1's load seeing y==2, then making the store of that speculative value visible to t2, circularly confirming y==2.)

There is another way for y.store(2) to execute, with both t1's statements running before either of t2's, but that leaves r1 == 1 not 2.

It's always a problem to try to prove anything by enumerating plausible program transformations / optimizations and saying that none of them will produce the result in question. The C++ abstract machine doesn't have to make sense in terms of causality or anything. But the OoTA circular-dependency rule is not part of the formalism so I don't know any other way than trying and failing to explain a result in a way that doesn't depend on inter-thread speculation leading to a speculation confirming itself or another speculation. That's basically my litmus test for this, and something real hardware doesn't do now and won't do in the future (unless something radically changes, like grouping multiple software threads together into one hardware thread, which is normally the opposite of what's helpful in a world where it's much cheaper to build 8 small cores than 1 big core that's 8x as fast.)


The only plausible and relevant transformation I see is value-range analysis on t1's r1 = y.load, to prove that it's always 1 or 2, not 0. A hypothetical compiler capable of inter-thread analysis could maybe decide that this load always runs before stores from other threads, letting it transform x.store(r1) to x.store(1) with no data-dependency on the load. But then it wouldn't need to do the load at all, and if r1 isn't optimized away, the only sane value would be a constant 1. Having r1 not equal to the value t1 stored to x isn't something that can happen in the abstract machine, so the as-if rule wouldn't allow transforming x.store(r1) to x.store(1) while still keeping the r1 = y.load as something that actually executes and could read a value other than 1.

You've already ruled out value-range analysis by using volatile std::atomic x, but I don't think you need to.


Even if I'm missing something in the above analysis, I don't see a way for r1==2 to happen on real hardware without inter-thread optimization that totally transforms the program into something totally different. Current compilers don't combine threads like that, so they couldn't even plausibly look for such an optimization.

The load / store in t1 has a data dependency (the store-data needs to wait for the load result). Even DEC Alpha couldn't make the store result visible before the load reads from cache, I don't think. (Alpha famously could violate causality in practice on some uarches for ptr = foo.load; ptr->load memory dependency ordering, making std::memoryorderconsume not free.) IIRC, the physical cause had something to do with another thread storing to the pointed-to location at the same time, and that store's visibility getting delayed by some cache-bank effect or something. Not by having the dependent load magically have the address before the first load finished, so no way for a store-data operation to happen before the load it depends on. (Except by a compiler transforming store(r1) to store(1) or store(2). But there's no chain of events that leads to x.store(2) in t1 so no way a compiler could statically nail that down as the only execution that can happen.)

All other ISAs (including AArch64 which you tagged) do have memory dependency-ordering rules which mean even load(consume)->load(relaxed) doesn't need extra memory fence instructions.

And as discussed in your previous question and Can the hardware reorder an atomic load followed by an atomic store, if the store is conditional on the load? - load / conditional-branch / store orders the store after the load on real hardware, and memory models like the Linux kernel's. The control dependency is like a memory fence because a core can't allow mis-speculated stores to commit to cache or otherwise become visible to other cores.