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 不知道 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。或者,如果手动启用。