如果未使用关键字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
之间的比较呢?看起来您忘了启用优化。 -O0
处理所有变量(register
变量除外)pretty similarly to volatile
for consistent debugging。
启用优化后,编译器可以从循环中提升非易失性负载。 while(locked);
将类似于源代码编译
if (locked) {
while(1){}
}
或者由于locked
有一个编译时常量初始化器,整个函数应编译为jmp main
(无限循环)。
有关详细信息,请参阅MCU programming - C++ O2 optimization breaks while loop。
为什么
DWORD PTR [RSP - 8]
被复制到EAX中,为什么在0和EAX之间进行比较?
当您使用volatile
时,有些编译器在将负载折叠到内存操作数以获取其他指令时更糟糕。我想这就是为什么你在这里得到一个单独的mov
负载;这只是一个错过的优化。
(虽然cmp [mem], imm
可能效率较低。我忘记它是否可以与JCC或其他东西进行宏融合。使用RIP相对寻址模式它不能微量融合负载,但是寄存器基础是可以的。)
cmp EAX, 0
很奇怪,我想与优化禁用的clang并不寻找test eax,eax
作为窥视优化来比较零。
正如@ user3386109所评论的那样,布尔上下文中的locked
等同于C / C ++中的locked != 0
。
编译器不知道缓存,它不是缓存的东西,它告诉编译器值可能在访问之间发生变化。因此,为了在功能上实现我们的代码,它需要按照我们要求的顺序执行我们要求的访问。无法优化。
void fun1 ( void )
{
/* volatile */ int lock = 999;
while (lock) continue;
}
void fun2 ( void )
{
volatile int lock = 999;
while (lock) continue;
}
volatile int vlock;
int ulock;
void fun3 ( void )
{
while(vlock) continue;
}
void fun4 ( void )
{
while(ulock) continue;
}
void fun5 ( void )
{
vlock=3;
vlock=4;
}
void fun6 ( void )
{
ulock=3;
ulock=4;
}
我发现手臂更容易看到......并不重要。
Disassembly of section .text:
00001000 <fun1>:
1000: eafffffe b 1000 <fun1>
00001004 <fun2>:
1004: e59f3018 ldr r3, [pc, #24] ; 1024 <fun2+0x20>
1008: e24dd008 sub sp, sp, #8
100c: e58d3004 str r3, [sp, #4]
1010: e59d3004 ldr r3, [sp, #4]
1014: e3530000 cmp r3, #0
1018: 1afffffc bne 1010 <fun2+0xc>
101c: e28dd008 add sp, sp, #8
1020: e12fff1e bx lr
1024: 000003e7 andeq r0, r0, r7, ror #7
00001028 <fun3>:
1028: e59f200c ldr r2, [pc, #12] ; 103c <fun3+0x14>
102c: e5923000 ldr r3, [r2]
1030: e3530000 cmp r3, #0
1034: 012fff1e bxeq lr
1038: eafffffb b 102c <fun3+0x4>
103c: 00002000
00001040 <fun4>:
1040: e59f3014 ldr r3, [pc, #20] ; 105c <fun4+0x1c>
1044: e5933000 ldr r3, [r3]
1048: e3530000 cmp r3, #0
104c: 012fff1e bxeq lr
1050: e3530000 cmp r3, #0
1054: 012fff1e bxeq lr
1058: eafffffa b 1048 <fun4+0x8>
105c: 00002004
00001060 <fun5>:
1060: e3a01003 mov r1, #3
1064: e3a02004 mov r2, #4
1068: e59f3008 ldr r3, [pc, #8] ; 1078 <fun5+0x18>
106c: e5831000 str r1, [r3]
1070: e5832000 str r2, [r3]
1074: e12fff1e bx lr
1078: 00002000
0000107c <fun6>:
107c: e3a02004 mov r2, #4
1080: e59f3004 ldr r3, [pc, #4] ; 108c <fun6+0x10>
1084: e5832000 str r2, [r3]
1088: e12fff1e bx lr
108c: 00002004
Disassembly of section .bss:
00002000 <vlock>:
2000: 00000000
00002004 <ulock>:
2004: 00000000
第一个是最有说服力的。
00001000 <fun1>:
1000: eafffffe b 1000 <fun1>
作为一个初始化的局部变量,非易失性,编译器可以假设它不会改变访问之间的值,因此它在while循环中永远不会改变,所以这实际上是一个1循环。如果初始值为零,则这将是一个简单的返回,因为它永远不会为非零,是非易失性的。
fun2是一个局部变量,然后需要构建堆栈帧
它执行代码尝试执行的操作,等待此共享变量,可以在循环期间更改
1010: e59d3004 ldr r3, [sp, #4]
1014: e3530000 cmp r3, #0
1018: 1afffffc bne 1010 <fun2+0xc>
所以它对它进行采样并测试每次循环时采样的内容。
fun3和fun4相同的交易,但更现实,因为功能代码的外部不会改变锁定,非全局对你的while循环没有多大意义。
102c: e5923000 ldr r3, [r2]
1030: e3530000 cmp r3, #0
1034: 012fff1e bxeq lr
1038: eafffffb b 102c <fun3+0x4>
对于volatile fun3情况,必须在每个循环中读取和测试变量
1044: e5933000 ldr r3, [r3]
1048: e3530000 cmp r3, #0
104c: 012fff1e bxeq lr
1050: e3530000 cmp r3, #0
1054: 012fff1e bxeq lr
1058: eafffffa b 1048 <fun4+0x8>
对于非易失性是全局的它必须对它进行一次采样,非常有趣的是编译器在这里做了什么,必须考虑它为什么会这样做,但无论哪种方式,你都可以看到“循环”重新测试存储在a中的值。注册(不缓存),永远不会改变正确的程序。在功能上我们要求它只使用非易失性读取变量一次然后无限期地测试该值。
fun5和fun6进一步证明volatile要求编译器在进入代码中的下一个操作/访问之前执行对其存储位置中变量的访问。所以当volatile时,我们要求编译器执行两个分配,即两个存储。当非易失性时,编译器可以优化第一个存储并且只执行最后一个存储,就好像您将代码视为一个整体一样,此函数(fun6)将变量设置为4,因此该函数将变量设置为4。
x86解决方案同样有趣repz retq遍布它(在我的计算机上使用编译器),不难发现它是什么。
aarch64,x86,mips,riscv,msp430,pdp11后端都不会对fun3()进行双重检查。
pdp11实际上是更容易阅读的代码(毫不奇怪)
00000000 <_fun1>:
0: 01ff br 0 <_fun1>
00000002 <_fun2>:
2: 65c6 fffe add $-2, sp
6: 15ce 03e7 mov $1747, (sp)
a: 1380 mov (sp), r0
c: 02fe bne a <_fun2+0x8>
e: 65c6 0002 add $2, sp
12: 0087 rts pc
00000014 <_fun3>:
14: 1dc0 0026 mov $3e <_vlock>, r0
18: 02fd bne 14 <_fun3>
1a: 0087 rts pc
0000001c <_fun4>:
1c: 1dc0 001c mov $3c <_ulock>, r0
20: 0bc0 tst r0
22: 02fe bne 20 <_fun4+0x4>
24: 0087 rts pc
00000026 <_fun5>:
26: 15f7 0003 0012 mov $3, $3e <_vlock>
2c: 15f7 0004 000c mov $4, $3e <_vlock>
32: 0087 rts pc
00000034 <_fun6>:
34: 15f7 0004 0002 mov $4, $3c <_ulock>
3a: 0087 rts pc
(这是未链接的版本)
cmp DWORD PTR [RSP - 8],0。 <---为什么与0完成比较,而DWORD PTR [RSP-8]在999内?
而真正的错误比较意义是它等于零或不等于零
为什么DWORD PTR [RSP-8]被复制到EAX中,为什么在0和EAX之间进行比较?
mov -0x8(%rsp),%eax
cmp 0,%eax
cmp 0,-0x8(%rsp)
as so.s -o so.o
so.s: Assembler messages:
so.s:3: Error: too many memory references for `cmp'
比较想要一个寄存器。所以它读入寄存器,因此它可以进行比较,因为它不能在一条指令中进行立即访问和内存访问之间的比较。如果他们可以在一条指令中完成它,那么他们就可以。