Loop fission/invariant 优化没有执行,为什么?
Loop fission/invariant optimization not performed, why?
我想了解更多关于汇编的知识,以及编译器可以做什么和不能做什么优化。
我有一段测试代码,对此我有一些疑问。
在此处查看实际操作: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 问题,但是代码没有机会在这里获取指向 argv 的指针。
- 在没有任何条件的循环中(汇编中的L3),为什么循环体是重复的?是为了减少跳转次数,同时仍然适合某种缓存?
GCC 不知道 printf
不会修改 argv
指向的内存,因此它无法将检查提升到循环之外。
argc
是局部变量(不能被任何指针全局变量指向),所以它知道调用不透明函数不能修改它。证明局部变量是真正私有的是 Escape Analysis.
的一部分
OP 通过首先将 argv[1][0]
复制到本地 char 变量中来对此进行测试:让 GCC 将完整条件提升到循环之外。
实际上 argv[1]
不会指向 printf
可以修改的内存。但我们只知道,因为 printf
是一个 C 标准库函数,并且我们假设 main
仅由 CRT 启动代码调用,并带有实际的命令行参数。不是通过该程序中传递其自己的参数的其他函数。在 C 中(与 C++ 不同),main
是可重入的,可以从程序内部调用。
此外,在 GNU C 中,printf
可以注册自定义格式字符串处理函数。虽然在这种情况下,编译器内置 printf
查看格式字符串并将其优化为 puts
调用。
所以 printf
已经部分特殊了,但我认为 GCC 不会费心去寻找基于它的优化,而不是修改任何其他全局可访问的内存。使用自定义 stdio 输出缓冲区,这甚至可能不是真的。 printf
很慢;在它周围节省一些溢出/重新加载通常不是什么大问题。
Would (theoretically) compiling puts() together with this main() allow the compiler to see puts() isn't touching argv and optimize the loop fully?
是的,例如如果您编写了自己的 write
函数,该函数在 syscall
指令周围使用内联 asm 语句(使用内存输入操作数使其安全,同时避免 "memory"
破坏)然后它可以内联并假设 argv[1][0]
没有被 asm 语句更改并基于它提升检查。即使你正在输出 argv[1]
.
或者在没有内联的情况下进行过程间优化。
回复:展开:这很奇怪,-funroll-loops
对于 -O3
的 GCC 默认不启用,只有 -O3 -fprofile-use
。或者,如果手动启用。
我想了解更多关于汇编的知识,以及编译器可以做什么和不能做什么优化。
我有一段测试代码,对此我有一些疑问。
在此处查看实际操作: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 问题,但是代码没有机会在这里获取指向 argv 的指针。
- 在没有任何条件的循环中(汇编中的L3),为什么循环体是重复的?是为了减少跳转次数,同时仍然适合某种缓存?
GCC 不知道 printf
不会修改 argv
指向的内存,因此它无法将检查提升到循环之外。
argc
是局部变量(不能被任何指针全局变量指向),所以它知道调用不透明函数不能修改它。证明局部变量是真正私有的是 Escape Analysis.
OP 通过首先将 argv[1][0]
复制到本地 char 变量中来对此进行测试:让 GCC 将完整条件提升到循环之外。
实际上 argv[1]
不会指向 printf
可以修改的内存。但我们只知道,因为 printf
是一个 C 标准库函数,并且我们假设 main
仅由 CRT 启动代码调用,并带有实际的命令行参数。不是通过该程序中传递其自己的参数的其他函数。在 C 中(与 C++ 不同),main
是可重入的,可以从程序内部调用。
此外,在 GNU C 中,printf
可以注册自定义格式字符串处理函数。虽然在这种情况下,编译器内置 printf
查看格式字符串并将其优化为 puts
调用。
所以 printf
已经部分特殊了,但我认为 GCC 不会费心去寻找基于它的优化,而不是修改任何其他全局可访问的内存。使用自定义 stdio 输出缓冲区,这甚至可能不是真的。 printf
很慢;在它周围节省一些溢出/重新加载通常不是什么大问题。
Would (theoretically) compiling puts() together with this main() allow the compiler to see puts() isn't touching argv and optimize the loop fully?
是的,例如如果您编写了自己的 write
函数,该函数在 syscall
指令周围使用内联 asm 语句(使用内存输入操作数使其安全,同时避免 "memory"
破坏)然后它可以内联并假设 argv[1][0]
没有被 asm 语句更改并基于它提升检查。即使你正在输出 argv[1]
.
或者在没有内联的情况下进行过程间优化。
回复:展开:这很奇怪,-funroll-loops
对于 -O3
的 GCC 默认不启用,只有 -O3 -fprofile-use
。或者,如果手动启用。