【发布时间】:2020-09-09 09:59:52
【问题描述】:
我正在尝试了解有关汇编的更多信息以及编译器可以做和不能做的优化。
我有一段测试代码有一些问题。
在此处查看实际操作:https://godbolt.org/z/pRztTT,或查看下面的代码和程序集。
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
for (int j = 0; j < 100; j++) {
if (argc == 2 && argv[1][0] == '5') {
printf("yes\n");
}
else {
printf("no\n");
}
}
return 0;
}
GCC 10.1 使用 -O3 生成的程序集:
.LC0:
.string "no"
.LC1:
.string "yes"
main:
push rbp
mov rbp, rsi
push rbx
mov ebx, 100
sub rsp, 8
cmp edi, 2
je .L2
jmp .L3
.L5:
mov edi, OFFSET FLAT:.LC0
call puts
sub ebx, 1
je .L4
.L2:
mov rax, QWORD PTR [rbp+8]
cmp BYTE PTR [rax], 53
jne .L5
mov edi, OFFSET FLAT:.LC1
call puts
sub ebx, 1
jne .L2
.L4:
add rsp, 8
xor eax, eax
pop rbx
pop rbp
ret
.L3:
mov edi, OFFSET FLAT:.LC0
call puts
sub ebx, 1
je .L4
mov edi, OFFSET FLAT:.LC0
call puts
sub ebx, 1
jne .L3
jmp .L4
似乎 GCC 产生了两种版本的循环:一种带有argv[1][0] == '5' 条件但没有argc == 2 条件,另一种没有任何条件。
我的问题:
- 是什么阻止了 GCC 拆分完整的条件?它类似于this question,但代码没有机会在这里获得指向 argv 的指针。
- 在没有任何条件的循环中(汇编中的L3),为什么循环体重复?是为了减少跳转次数,同时仍然适合某种缓存吗?
【问题讨论】:
-
我猜 GCC 不知道
printf不会修改argv指向的内存。main和printf/puts需要特殊规则才能知道char **arg 永远不会直接或间接指向某些名为puts的非内联函数调用可能修改的内存。回复:展开:这很奇怪,-O3的 GCC 默认不启用-funroll-loops,仅使用-O3 -fprofile-use -
@PeterCordes:感谢您提供的信息。当我修改程序以首先将
argv[1][0]复制到本地char变量中时,GCC 确实将完整条件移到了循环之外。 (理论上)编译puts()和这个main()是否允许编译器看到puts()没有触及argv并完全优化循环? -
是的,例如如果您编写了自己的
write函数,该函数在syscall指令周围使用内联asm语句,并带有内存输入操作数(并且没有"memory"clobber),那么它可以内联。 (或者可能在没有内联的情况下进行过程间优化。) -
仅供参考,我找到了导致重复循环体的原因:
-freorder-blocks-algorithm=stc:'stc',“软件跟踪缓存”算法,它试图将所有经常执行的代码放在一起,最小化通过制作额外的代码副本执行的分支数。
标签: c gcc assembly optimization loop-invariant