如果一个变量没有用关键字volatile
指定,编译器可能会缓存。变量 * 必须 * 总是从内存访问,否则直到它的事务单元结束。我想知道的一点在于汇编部分。
int main() {
/* volatile */ int lock = 999;
while (lock);
}
在x86-64-clang-3.0.0 compiler上,其汇编代码如下所示。
main: # @main
mov DWORD PTR [RSP - 4], 0
mov DWORD PTR [RSP - 8], 999
.LBB0_1: # =>This Inner Loop Header: Depth=1
cmp DWORD PTR [RSP - 8], 0
je .LBB0_3
jmp .LBB0_1
.LBB0_3:
mov EAX, DWORD PTR [RSP - 4]
ret
当在中注解volatile
关键字时,结果如下。
main: # @main
mov DWORD PTR [RSP - 4], 0
mov DWORD PTR [RSP - 8], 999
.LBB0_1: # =>This Inner Loop Header: Depth=1
mov EAX, DWORD PTR [RSP - 8]
cmp EAX, 0
je .LBB0_3
jmp .LBB0_1
.LBB0_3:
mov EAX, DWORD PTR [RSP - 4]
ret
我想知道和不明白的地方,
cmp DWORD PTR [RSP - 8], 0
.〈---为什么用0
进行比较,而DWORD PTR [RSP - 8]
将999
保持在范围内?- 为什么要将
DWORD PTR [RSP - 8]
复制到EAX
中,为什么要在0
和EAX
之间进行比较?
2条答案
按热度按时间aemubtdh1#
看起来您忘记启用优化。
-O0
处理 * 所有 * 变量(register
变量除外)pretty similarly tovolatile
for consistent debugging。启用优化后,编译器可以将非易失性加载从循环中提升出来。
while(locked);
的编译类似于源代码,如或者因为
locked
有一个编译时常量初始化器,所以整个函数应该编译成jmp main
(一个无限循环)。有关更多详细信息,请参见MCU编程- C++ O2优化中断while循环。
为什么要将
DWORD PTR [RSP - 8]
复制到EAX中,为什么要在0和EAX之间进行比较?当你使用
volatile
时,一些编译器在将加载合并到其他指令的内存操作数方面做得更差。这只是一个错过的优化。(虽然
cmp [mem], imm
可能效率 * 较低 *。我忘了它是否可以与JCC或其他东西宏融合。使用RIP相对寻址模式,它不能微融合加载,但寄存器基址是可以的。)cmp EAX, 0
是奇怪的,我猜优化被禁用的clang不会将test eax,eax
作为与零进行比较的窥视孔优化。如@user3386109所述,布尔上下文中的
locked
等效于C / C++中的locked != 0
。lp0sw83n2#
编译器不知道缓存,它不是一个缓存的东西,它告诉编译器,值可能会在访问之间改变。因此,为了功能性地实现我们的代码,它需要按照我们要求的顺序执行我们要求的访问。不能优化。
我觉得戴着手臂更容易看清......没关系。
第一条最能说明问题:
作为一个被初始化的非易失性局部变量,编译器可以假设它在两次访问之间不会改变值,所以它在while循环中永远不会改变,所以这本质上是一个while 1循环。如果初始值是零,这将是一个简单的返回,因为它永远不会是非零的,是非易失性的。
fun 2是局部变量,则需要构建堆栈帧。
它做的是假设代码试图做的事情,等待这个共享变量,一个可以在循环过程中改变的变量
因此它对它进行采样,并测试每次通过循环时采样的内容。
fun 3和fun 4处理相同,但更实际,因为函数代码的外部不会更改lock,非全局性对while循环没有多大意义。
对于volatile fun 3情况,必须在每个循环中读取和测试变量
因为非易失性变量是全局变量,所以它必须对它进行一次采样,编译器在这里所做的非常有趣,必须考虑为什么它要这样做,但无论如何,你可以看到“循环”重新测试存储在寄存器(不是缓存的)中的值,该值永远不会随着适当的程序而改变。从功能上讲,我们要求它通过使用非易失性变量只读取变量一次,然后它会无限期地测试该值。
fun 5和fun 6进一步说明了volatile要求编译器在执行下一个操作/访问代码之前,先在变量的存储位置执行对变量的访问。非易失性时,编译器可以优化第一个存储,而只优化最后一个存储,就好像您将代码作为一个整体来看待此函数(fun 6)会将变数设定为4,因此函数会将变数设定为4。
x86解决方案同样有趣,repz retq到处都是(用我电脑上的编译器),不难找出那是怎么回事。
aarch 64、x86、mips、riscv、msp430和pdp 11后端都不会对fun 3()进行双重检查。
pdp 11实际上是更容易阅读的代码(这并不奇怪)
(this为未链接版本)
cmp DWORD PTR [RSP - 8],0 .〈---为什么用0进行比较,而DWORD PTR [RSP - 8]的值在999以内?
而"真“”假“比较表示它等于零还是不等于零
为什么要将DWORD PTR [RSP - 8]复制到EAX中,为什么要在0和EAX之间进行比较?
比较需要一个寄存器,所以它读入一个寄存器,这样它就可以进行比较了,因为它不能在一条指令中完成立即数和内存访问之间的比较,如果他们能在一条指令中完成的话,他们早就做了。