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,循环中没有函数调用,我只能看到它从内存中加载一次标志。
循环中的函数调用似乎可以神奇。
对此有什么解释吗?
因为 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
已经非常昂贵,以至于几个额外的加载指令的成本很可能会在噪音中丢失。