我有一个简单的C++代码片段,如下所示:
int A;
int B;
void foo() {
A = B + 1;
// asm volatile("" ::: "memory");
B = 0;
}
当我编译这段代码时,生成的汇编代码被重新排序如下:
foo():
mov eax, DWORD PTR B[rip]
mov DWORD PTR B[rip], 0
add eax, 1
mov DWORD PTR A[rip], eax
ret
B:
.zero 4
A:
.zero 4
但是,当我添加内存围栏(C++代码中的注解行)时,指令不会重新排序。我的理解是,向变量添加volatile
限定符也应该防止指令重新排序。因此,我修改了代码,将volatile
添加到变量B:
int A;
volatile int B;
void foo() {
A = B + 1;
B = 0;
}
令我惊讶的是,生成的汇编代码仍然显示重新排序的指令。有人能解释一下为什么volatile
限定符在这种情况下没有阻止指令重新排序吗?
代码在godbolt中可用
1条答案
按热度按时间zrfyljdw1#
我的理解是,向变量添加volatile限定符也应该防止指令重新排序。
这是一个重大的过于简单化。虽然C标准没有非常明确地定义
volatile
的语义(只说“访问严格根据抽象机器的规则进行评估”),但不成文的规则是,volatile
对象被视为某个外部实体(例如:I/O硬件)可以异步地阅读和写入它们,并且读取和写入都是外部实体可以观察到的副作用。因此,对volatile
对象(机器字大小或更小)的每次读取/写入应导致执行恰好一个加载/存储指令。由此可见,对
volatile
对象的加载和存储将不会彼此重新排序**。但是在你的程序中A
不是volatile的,所以我们假设外部实体看不到它。因此,对A
的访问相对于对B
或其他任何内容的访问如何排序并不重要,编译器可以自由地对它们进行重新排序。像add eax, 1
这样根本不访问内存的指令也是公平的;外部实体也看不到机器寄存器。根据你对concurrency标签的使用,这是
volatile
不是线程间共享变量的正确方法的众多原因之一--因为与“外部实体”不同,另一个线程 * 确实 * 可以访问你的非volatile
变量。在C11之前的奥登时代,人们使用volatile
,因为它是所有的,如果你知道一些关于编译器优化的方式(通常没有文档),你可以使用显式内存屏障函数来使它工作。从C++11开始,我们就有了std::atomic
,这是处理线程间共享的唯一正确方法,但不幸的是,与volatile
的关联在过时的文档和老前辈的头脑中挥之不去。Why is volatile not considered useful in multithreaded C or C++ programming?更多还相关:Does the C++ volatile keyword introduce a memory fence?(不,正如你所发现的,它不会。)