理解“易变”关键词和比较是如何工作的

问题描述 投票:0回答:2

如果未使用关键字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], 00,而DWORD PTR [RSP - 8]持有999
  • 为什么DWORD PTR [RSP - 8]被复制到EAX,为什么0EAX之间的比较呢?
c assembly x86-64 volatile
2个回答
5
投票

看起来您忘了启用优化。 -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


2
投票

编译器不知道缓存,它不是缓存的东西,它告诉编译器值可能在访问之间发生变化。因此,为了在功能上实现我们的代码,它需要按照我们要求的顺序执行我们要求的访问。无法优化。

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'

比较想要一个寄存器。所以它读入寄存器,因此它可以进行比较,因为它不能在一条指令中进行立即访问和内存访问之间的比较。如果他们可以在一条指令中完成它,那么他们就可以。

© www.soinside.com 2019 - 2024. All rights reserved.