我正在运行一个运行的线程,直到设置了一个标志。
std::atomic<bool> stop(false);
void f() {
while(!stop.load(std::memory_order_{relaxed,acquire})) {
do_the_job();
}
}
我想知道编译器是否可以像这样展开循环(我不希望它发生)。
void f() {
while(!stop.load(std::memory_order_{relaxed,acquire})) {
do_the_job();
do_the_job();
do_the_job();
do_the_job();
... // unroll as many as the compiler wants
}
}
据说波动性和原子性是正交的,但我有点困惑。编译器是否可以自由缓存原子变量的值并展开循环?如果编译器可以展开循环,那么我认为我必须将volatile
放到标志上,我想确定。
我应该放volatile
吗?
我很抱歉暧昧。我(我猜我)理解重新排序是什么以及memory_order_*
是什么意思,我确信我完全理解volatile
是什么。
我认为while()
循环可以转换为这样的无限if
语句。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
...
}
由于给定的内存命令不会阻止序列化前的操作移过原子载荷,我认为如果它没有易失性,它可以重新排列。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
if(stop.load(std::memory_order_{relaxed,acquire})) return;
if(stop.load(std::memory_order_{relaxed,acquire})) return;
...
do_the_job();
do_the_job();
do_the_job();
...
}
如果原子并不意味着易失性,那么我认为在最坏的情况下代码甚至可以像这样转换。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
while(true) {
do_the_job();
}
}
永远不会有这样疯狂的实施,但我想它仍然是一种可能的情况。我认为防止这种情况的唯一方法是将volatile
放入原子变量并询问它。
我做了很多猜测,请告诉我他们之间是否有任何问题。
答案 0 :(得分:7)
编译器是否可以自由缓存原子变量的值并展开循环?
编译器无法缓存原子变量的值。
但是,由于您使用的是std::memory_order_relaxed
,这意味着编译器可以自由地对此原子变量的加载和存储进行重新排序,与其他加载和存储相关。
另请注意,对此转换单元中定义不可用的函数的调用是编译器内存屏障。这意味着调用不能对周围的加载和存储进行重新排序,并且所有非局部变量必须在调用后从内存中重新加载,就好像它们都标记为volatile一样。 (不会重新加载其地址未在其他地方传递的局部变量)。
您希望避免的代码的转换不会是有效的转换,因为这会违反C ++内存模型:在第一种情况下,您有一个原子变量的加载,然后调用do_the_job
,在第二个中,您有多个电话。观察到的转换代码的行为可能不同。
来自std::memory_order的说明:
与易失性的关系
在执行的一个线程中,对所有易失性对象的访问(读取和写入)保证不会相对于彼此重新排序,但是这个顺序不能保证被另一个线程观察到,因为volatile的访问不会建立inter - 线程同步。
此外,易失性访问不是原子的(并发读取和写入是数据竞争)并且不命令存储器(非易失性存储器访问可以在易失性访问周围自由重新排序)。 / p>
这个位非易失性存储器访问可以在易失性访问周围自由重新排序对于轻松原子也是如此,因为可以对其他加载和存储重新排序宽松的加载和存储。
换句话说,使用volatile
装饰原子不会改变代码的行为。
无论如何,C ++ 11原子变量不需要用volatile
关键字标记。
以下是g ++ - 5.2如何表彰原子变量的示例。以下功能:
__attribute__((noinline)) int f(std::atomic<int>& a) {
return a.load(std::memory_order_relaxed);
}
__attribute__((noinline)) int g(std::atomic<int>& a) {
static_cast<void>(a.load(std::memory_order_relaxed));
static_cast<void>(a.load(std::memory_order_relaxed));
static_cast<void>(a.load(std::memory_order_relaxed));
return a.load(std::memory_order_relaxed);
}
__attribute__((noinline)) int h(std::atomic<int>& a) {
while(a.load(std::memory_order_relaxed))
;
return 0;
}
使用g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S
编译生成以下程序集:
f(std::atomic<int>&):
movl (%rdi), %eax
ret
g(std::atomic<int>&):
movl (%rdi), %eax
movl (%rdi), %eax
movl (%rdi), %eax
movl (%rdi), %eax
ret
h(std::atomic<int>&):
.L4:
movl (%rdi), %eax
testl %eax, %eax
jne .L4
ret
答案 1 :(得分:2)
如果do_the_job()
没有更改stop
,那么编译器是否可以展开循环并不重要。
std::memory_order_relaxed
只是确保每个操作都是原子操作,但它不会阻止重新排序访问。这意味着如果另一个线程将stop
设置为true
,则循环可以继续执行几次,因为可以重新排序访问。所以它与展开循环的情况相同:do_the_job()
可能在另一个线程将stop
设置为true
后执行几次。
所以不,不要使用volatile
,请使用std::memory_order_acquire
和std::memory_order_release
。