为什么 gcc 12.2 不优化从 main() 调用的 constexpr 函数中的除法到移位

Fin*_*ers 4 c++ g++ integer-division compiler-optimization constexpr-function

我一直在使用 Godbolt 编译器并输入以下代码:

constexpr int func(int x)
{
    return x > 3 ? x * 2 : (x < -4 ? x - 4 : x / 2);
}

int main(int argc)
{
    return func(argc);
}
Run Code Online (Sandbox Code Playgroud)

代码有点简单。这里重要的部分是里面最后除以2 func(int x)。由于 x 是一个整数,基本上任何编译器都会将其简化为移位以避免除法指令。

x86-64 gcc 12.2(对于 Linux,因此 System V ABI)的程序集-O3如下所示:

main:
        cmp     edi, 3
        jle     .L2
        lea     eax, [rdi+rdi]
        ret
.L2:
        cmp     edi, -4
        jge     .L4
        lea     eax, [rdi-4]
        ret
.L4:
        mov     eax, edi
        mov     ecx, 2
        cdq
        idiv    ecx
        ret
Run Code Online (Sandbox Code Playgroud)

您可以看到最终的idiv ecx命令不是移位,而是实际除以 2。我还测试了 clang,并且 clang 实际上将其减少为移位。

main:                                   # @main
        mov     eax, edi
        cmp     edi, 4
        jl      .LBB0_2
        add     eax, eax
        ret
.LBB0_2:
        cmp     eax, -5
        jg      .LBB0_4
        add     eax, -4
        ret
.LBB0_4:
        mov     ecx, eax
        shr     cl, 7
        add     cl, al
        sar     cl
        movsx   eax, cl
        ret
Run Code Online (Sandbox Code Playgroud)

这可能是由于内联吗?我很好奇这里发生了什么。

Pet*_*des 15

GCCmain特别对待:隐式__attribute__((cold))

因此main得到的优化较少(或者更注重大小而不是速度),因为它在大多数程序中通常只调用一次。 与(优化大小)__attribute__((cold))不太一样,但这是朝这个方向迈出的一步,有时会得到成本启发来选择一个朴素的除法指令。-Os

正如GCC 开发人员 Marc Glisse 评论的那样,如果您正在对代码进行基准测试或查看其优化方式,请不要将代码放入调用的函数中。main (除了 之外,还可能有其他特殊的东西,例如 MinGW GCC在 init 函数中cold添加了额外的内容,并添加了代码以将堆栈对齐 16。所有这些都是您正在查看的代码中不希望看到的噪音。另请参阅如何从 GCC/clang 程序集输出中删除“噪音”?callgcc -m32

另一个问答显示 GCC 放入main了该.text.startup部分,以及其他假定的“冷”功能。(这对于 TLB 和分页局部性有好处;希望在进程启动后可以逐出一整页的 init 函数。这个想法是代码可能main只运行一次,真正的工作发生在它调用的某些函数内。这可能不是真的如果实际工作内联到 main 中,或者对于简单的程序。)

对于所有代码都在 中的玩具程序来说,这是一个不好的启发式方法main,但这就是 GCC 所做的。人们经常运行的大多数真实程序都不是玩具,并且在某些其他函数中拥有足够的代码,而这些代码没有内联到main. 如果启发式方法更聪明一点并且删除cold整个程序或循环中的所有函数确实优化为main,那就太好了,因为一些实际的程序非常简单。

您可以使用GNU C 函数属性覆盖启发式。

  • __attribute__((hot)) int main(){ ...优化您期望的方式
    Sopel 评论中的Godbolt,添加了属性)。
  • __attribute__((cold))在一个未调用的函数上mainProduce idiv
  • __attribute__((optimize("O3")))没有帮助。

int main(int x, char **y){ return x/2; } 仍然使用 的转变gcc -O2,所以主要存在cold并不总是有这种效果(与 不同-Os)。

但也许你的除法已经是有条件的,GCC 猜测基本块甚至不会每次都运行,所以更有理由让它小而不是快。


-Os疯狂的是, x86-64 (Godbolt)的GCC确实使用idiv常量 2 进行有符号除法,而不仅仅是任意常量(其中 GCC 通常甚至在 处使用乘法逆元-O0)。如果任何代码大小与带有修正舍入到零(而不是 -inf)的算术右移相比,它不会节省太多,并且可能会慢得多,特别是对于 Ice Lake 之前的 Intel 上的 64 位整数。AArch64 也是如此,无论哪种方式都是 2 个固定大小的指令,而且sdiv几乎肯定会慢得多。

sdiv确实在 AArch64 上节省了一些代码大小,以获得更高的 2 次幂(Godbolt),但仍然慢得多,这可能不是一个很好的权衡-Osidiv不保存 x86-64 上的指令(因为cdq需要将cqo指令保存到 RDX 中),尽管代码大小可能只有几个字节。因此,可能只适用于-Oz使用push 2/pop rcx将一个小常量放入3 字节 x86-64 机器代码而不是 5 字节的寄存器中的情况。


归档时间:

查看次数:

246 次

最近记录:

2 年,10 月 前