assembly 为什么给变量添加volatile限定符不能防止指令重新排序?

dxpyg8gm  于 2023-06-30  发布在  其他
关注(0)|答案(1)|浏览(80)

我有一个简单的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中可用

zrfyljdw

zrfyljdw1#

我的理解是,向变量添加volatile限定符也应该防止指令重新排序。
这是一个重大的过于简单化。虽然C标准没有非常明确地定义volatile的语义(只说“访问严格根据抽象机器的规则进行评估”),但不成文的规则是,volatile对象被视为某个外部实体(例如:I/O硬件)可以异步地阅读和写入它们,并且读取和写入都是外部实体可以观察到的副作用。因此,对volatile对象(机器字大小或更小)的每次读取/写入应导致执行恰好一个加载/存储指令。
由此可见,对volatile对象的加载和存储将不会彼此重新排序**。但是在你的程序中A不是volatile的,所以我们假设外部实体看不到它。因此,对A的访问相对于对B或其他任何内容的访问如何排序并不重要,编译器可以自由地对它们进行重新排序。像add eax, 1这样根本不访问内存的指令也是公平的;外部实体也看不到机器寄存器。
根据你对concurrency标签的使用,这是volatile不是线程间共享变量的正确方法的众多原因之一--因为与“外部实体”不同,另一个线程 * 确实 * 可以访问你的非volatile变量。在C
11之前的奥登时代,人们使用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?(不,正如你所发现的,它不会。)

相关问题