提问者:小点点

非易失性全局变量在循环中工作


  1 #include <stdio.h>
  2 #include <stdbool.h>
  3 
  4 bool flag;
  5 
  6 static void foo(int a, int b)
  7 {
  8     printf("why\n");
  9     return;
 10 }
 11 
 12 int main()
 13 {
 14 
 15     while (!flag) {
 16         foo(10, 11);
 17     }
 18 
 19     return 0;
 20 }

使用 aarch64-linux-gnu-gcc -O2 t.c 构建

objdump with aarch64-linux-gnu-objdump -Sdf a.out

 55 0000000000400460 <main>:
 56   400460:   a9be7bfd    stp x29, x30, [sp, #-32]!
 57   400464:   910003fd    mov x29, sp
 58   400468:   f9000bf3    str x19, [sp, #16]
 59   40046c:   b0000093    adrp    x19, 411000 <__libc_start_main@GLIBC_2.17>
 60   400470:   3940c660    ldrb    w0, [x19, #49]
 61   400474:   35000140    cbnz    w0, 40049c <main+0x3c>
 62   400478:   f9000fb4    str x20, [x29, #24]
 63   40047c:   9100c673    add x19, x19, #0x31
 64   400480:   90000014    adrp    x20, 400000 <_init-0x3e8>
 65   400484:   91198294    add x20, x20, #0x660
 66   400488:   aa1403e0    mov x0, x20
 67   40048c:   97fffff1    bl  400450 <puts@plt>
 68   400490:   39400260    ldrb    w0, [x19]
 69   400494:   34ffffa0    cbz w0, 400488 <main+0x28>
 70   400498:   f9400fb4    ldr x20, [x29, #24]
 71   40049c:   52800000    mov w0, #0x0                    // #0
 72   4004a0:   f9400bf3    ldr x19, [sp, #16]
 73   4004a4:   a8c27bfd    ldp x29, x30, [sp], #32
 74   4004a8:   d65f03c0    ret

我担心的是为什么#68总是从内存中加载标志?它不是易失性类型,它不是只从 mem 加载一次然后从寄存器读取吗?如果我删除 C 代码 #16,循环中没有函数调用,我只能看到它从内存中加载一次标志。

循环中的函数调用似乎可以神奇。

对此有什么解释吗?


共2个答案

匿名用户

因为 flag 具有外部链接,编译器不能假设它不会在执行过程中从另一个翻译单元更新。

标志更改为静态或使其成为本地,然后整个程序将被一遍又一遍的永恒循环调用 put 所取代。

编辑:原始代码的 gcc 12.1 为 ARM64 -O3 进行相关反汇编:

.L3:
        mov     x0, x20
        bl      puts
        ldrb    w0, [x19]
        cbz     w0, .L3

标志更改为静态会创建一个永恒的循环:

.L2:
        mov     x0, x19
        bl      puts
        b       .L2

保留标志作为外部链接,但注释掉函数调用:

.L3:
        b       .L3

最后一个发生,因为如果删除函数调用,循环体不再包含打印等副作用。然后检查变量是没有意义的。

匿名用户

只是比Lundin的回答更明确地说:编译器担心printf可能会修改标志

通常,当调用其源代码当前对编译器不可见的任何代码时(例如,因为它是在另一个源文件中定义的),编译器必须假设它可以执行定义良好的 C 代码可能执行的任何操作,包括修改全局变量。标准库函数(如 printf)通常不能免除此假设。

也就是说,由于标准库函数的行为由 C 标准定义,因此如果编译器作者想要实现它,编译器实际上可以做出一些假设。有一些基于此类假设的常见优化;例如,数学函数除了可能设置 errno 外没有副作用。事实上,printf 有一些常见的优化,例如,当格式字符串是常量、不包含格式说明符并以 \n 结尾时,将其替换为 put;这发生在 Lundin 的示例代码中。

因此,原则上,理想的编译器可以利用 printf 定义为不修改随机全局变量的事实,并优化 flag 的重新加载。但这将是一个非常专业的优化,其好处可能不值得实现它的成本。相对而言,调用 printf 已经非常昂贵,以至于几个额外的加载指令的成本很可能会在噪音中丢失。