I have read about std::memory_order
in C++ and understood partially. But I still had some doubts around it.
std::memory_order_acquire
says that, no reads or writes in the current thread can be reordered before this load. Does that mean compiler and cpu is not allowed to move any instruction present below the acquire
statement, above it?auto y = x.load(std::memory_order_acquire);
z = a; // is it leagal to execute loading of shared `b` above acquire? (I feel no)
b = 2; // is it leagal to execute storing of shared `a` above acquire? (I feel yes)
I can reason out why it is illegal for executing loads before acquire
. But why it is illegal for stores?
atomic
objects? Since they are not volatile
, and as I know only volatile has this requirement.auto y = x.load(std::memory_order_acquire); // `y` is never used
return;
This optimization is not happening even with relaxed
memory order.
acquire
statement, below it?z = a; // is it leagal to execute loading of shared `b` below acquire? (I feel yes)
b = 2; // is it leagal to execute storing of shared `a` below acquire? (I feel yes)
auto y = x.load(std::memory_order_acquire);
acquire
boundary?auto y = x.load(std::memory_order_acquire);
a = p; // can this move below the below line?
b = q; // shared `a` and `b`
Similar and corresponding 4 doubts with release
semantics also.
Related to 2nd and 3rd question, why no compiler is optimizing f()
, as aggressive as g()
in below code?
#include <atomic>
int a, b;
void dummy(int*);
void f(std::atomic<int> &x) {
int z;
z = a; // loading shared `a` before acquire
b = 2; // storing shared `b` before acquire
auto y = x.load(std::memory_order_acquire);
z = a; // loading shared `a` after acquire
b = 2; // storing shared `b` after acquire
dummy(&z);
}
void g(int &x) {
int z;
z = a;
b = 2;
auto y = x;
z = a;
b = 2;
dummy(&z);
}
f(std::atomic<int>&):
sub rsp, 24
mov eax, DWORD PTR a[rip]
mov DWORD PTR b[rip], 2
mov DWORD PTR [rsp+12], eax
mov eax, DWORD PTR [rdi]
lea rdi, [rsp+12]
mov DWORD PTR b[rip], 2
mov eax, DWORD PTR a[rip]
mov DWORD PTR [rsp+12], eax
call dummy(int*)
add rsp, 24
ret
g(int&):
sub rsp, 24
mov eax, DWORD PTR a[rip]
mov DWORD PTR b[rip], 2
lea rdi, [rsp+12]
mov DWORD PTR [rsp+12], eax
call dummy(int*)
add rsp, 24
ret
b:
.zero 4
a:
.zero 4
Generally, yes. Any load or store that follows (in program order) an acquire load, must not become visible before it.
Here is an example where it matters:
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> x{0};
std::atomic<bool> finished{false};
int xval;
bool good;
void reader() {
xval = x.load(std::memory_order_relaxed);
finished.store(true, std::memory_order_release);
}
void writer() {
good = finished.load(std::memory_order_acquire);
x.store(42, std::memory_order_relaxed);
}
int main() {
std::thread t1(reader);
std::thread t2(writer);
t1.join();
t2.join();
if (good) {
std::cout << xval << std::endl;
} else {
std::cout << "too soon" << std::endl;
}
return 0;
}
This program is free of UB and must print either 0
or too soon
. If the writer
store of 42 to x
could be reordered before the load of finished
, then it would be possible that the reader
load of x
returns 42 and the writer
load of finished
returns true
, in which case the program would improperly print 42
.
Yes, it would be okay for a compiler to delete the atomic load whose value is never used, since there is no way for a conforming program to detect whether the load was done or not. However, current compilers generally don't do such optimizations. Partly out of an abundance of caution, because optimizations on atomics are hard to get right and bugs can be very subtle. It may also be partly to support programmers writing implementation-dependent code, that is able to detect via non-standard features whether the load was done.
Yes, this reordering is perfectly legal, and real-world architectures will do it. An acquire barrier is only one way.
Yes, this is also legal. If a,b
are not atomic, and some other thread is reading them concurrently, then the code has a data race and is UB, so it is okay if the other thread observes the writes having happened in the wrong order (or summons nasal demons). (If they are atomic and you are doing relaxed stores, then you can't get nasal demons, but you can still observe the stores out of order; there is no happens-before relationship mandating the contrary.)
Your f
versus g
examples is not really a fair comparison: in g
, the load of the non-atomic variable x
has no side effects and its value is not used, so the compiler omits it altogether. As mentioned above, the compiler doesn't omit the unnecessary atomic load of x
in f
.
As to why the compilers don't sink the first accesses to a
and b
past the acquire load: I believe it's simply a missed optimization. Again, most compilers deliberately don't try to do all possible legal optimizations with atomics. However, you could note that on ARM64 for instance, the load of x
in f
compiles to ldar
, which the CPU can definitely reorder with earlier plain loads and stores