【问题标题】:Loop fission/invariant optimization not performed, why?未执行循环裂变/不变优化,为什么?
【发布时间】: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 指向的内存。 mainprintf / 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


【解决方案1】:

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 很慢;节省一些溢出/重新加载通常没什么大不了的。


将 puts() 与 main() 一起编译(理论上)是否允许编译器看到 puts() 没有触及 argv 并完全优化循环?

是的,例如如果您编写了自己的 write 函数,该函数在 syscall 指令周围使用内联 asm 语句(使用仅内存输入操作数使其安全,同时避免 "memory" clobber),那么它可以内联并假设argv[1][0] 没有被 asm 语句更改并根据它进行检查。即使你输出的是argv[1]

或者可以在不进行内联的情况下进行过程间优化。


Re:展开:这很奇怪,-O3 的 GCC 默认不启用-funroll-loops,只有-O3 -fprofile-use。或者如果手动启用。

【讨论】:

  • 仅供参考,我找到了导致重复循环体的原因:-freorder-blocks-algorithm=stc:'stc',“软件跟踪缓存”算法,它试图将所有经常执行的代码放在一起,最小化通过制作额外的代码副本执行的分支数。
猜你喜欢
  • 2018-11-01
  • 1970-01-01
  • 2012-06-22
  • 2021-03-24
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 2018-07-12
  • 2017-01-31
相关资源
最近更新 更多