为何volatile访问可与非volatile访问发生重排序?
这个问题问得特别准,刚好戳中了很多开发者对volatile的常见误解——不少人觉得它能搞定所有内存顺序的问题,但其实它的能力边界很明确。咱们一步步拆解来看:
先还原代码与汇编
首先把你的C代码和生成的汇编清晰列出来,方便对照:
// cordering.c // 编译命令: gcc -O2 -S -masm=intel cordering.c int A; volatile int B; void foo() { A = B + 1; B = 0; }
对应的汇编代码:
mov eax, DWORD PTR B[rip] ; 读取volatile变量B的值到寄存器eax mov DWORD PTR B[rip], 0 ; 将volatile变量B设为0 add eax, 1 ; 计算原来的B值+1 mov DWORD PTR A[rip], eax ; 把结果赋值给非volatile变量A ret
为什么会出现这种重排序?
核心原因是**volatile的语义只约束它自己,不管非volatile变量的访问顺序**:
volatile到底管什么? 它的核心作用是告诉编译器:「这个变量可能被当前线程之外的东西修改(比如硬件中断、其他线程),你不能对它的读写操作瞎优化」。具体来说:编译器不能省略对volatile变量的读写,不能把它的值缓存到寄存器后重复用(除非是同一个序列点内的连续访问),必须严格按照代码里的顺序执行对volatile变量的所有读写操作。- 非volatile变量的“自由” 对于像
A这样的非volatile变量,编译器默认假设只有当前线程会碰它。只要最终程序的「可观测行为」(仅从volatile变量和I/O操作的角度看)不变,编译器可以随便调整非volatile变量访问的顺序,甚至做其他优化——毕竟这不会影响程序的正确输出。
回到你的例子:
原C代码里,对B的操作顺序是先读B(用来算A),再写B=0——汇编里对B的操作完全遵循这个顺序(先读再写),完全没违反volatile的规则。而对A的赋值,因为A是非volatile的,编译器知道把它放在B=0之后,不会改变程序的可观测结果(毕竟没人会盯着A的中间状态),所以就做了这个重排——本质上是编译器为了效率(比如利用寄存器减少内存访问)做的合理优化。
关键结论
volatile不是内存屏障,它不保证volatile访问和非volatile访问之间的顺序,只保证volatile变量自身的读写顺序不被编译器打乱。如果是多线程场景需要严格的内存顺序控制,别用volatile,应该用C标准<stdatomic.h>里的原子操作和内存序约束。
内容的提问来源于stack exchange,提问作者choxsword
相关产品推荐
相关产品推荐

