c++ 为什么GCC12.2没有在这个从main()调用的constexpr函数中优化移位除法

2eafrhcq  于 2023-08-09  发布在  其他
关注(0)|答案(1)|浏览(77)

我一直在玩Godbolt编译器,并输入了以下代码:

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

int main(int argc)
{
    return func(argc);
}

字符串
代码有点直接。这里最重要的部分是func(int x)内部的最后一次除以2。由于x是一个整数,基本上任何编译器都会将其简化为移位以避免除法指令。
来自x86-64 gcc 12.2 -O3(对于Linux,因此是System V ABI)的程序集看起来像这样:

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


您可以看到最后的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


这可能是由于内联造成的吗?我对这里发生的事情很好奇。

zphenhs4

zphenhs41#

GCC对main有特殊处理:隐式__attribute__((cold))

因此main得到的优化较少(或者更倾向于大小而不是速度),因为它在大多数程序中通常只被调用一次。__attribute__((cold))-Os(优化大小)并不完全相同,但它是朝着这个方向迈出的一步,有时会获得成本启发法来选择一个简单的除法指令。
正如GCC开发人员Marc Glisse评论的那样,**如果你正在对main进行基准测试或查看它是如何优化的,不要将你的代码放在一个名为main的函数中。MinGW GCC在init函数中添加了一个额外的callgcc -m32添加了代码以将堆栈对齐16。所有这些都是您不希望看到的代码中的噪声。参见How to remove "noise" from GCC/clang assembly output?
Another Q&A显示GCC将main放入.text.startup部分,沿着其他假定的“冷”函数。(这对于TLB和分页局部性是有好处的;希望在进程启动后,整个init函数页都能被逐出。这个想法是,main中的代码可能只运行一次,真实的工作发生在它调用的某些函数中。如果真实的工作内联到main中,或者对于简单的程序,这可能不是真的。
对于所有代码都是main的玩具程序来说,这是一个糟糕的启发式方法,但GCC就是这样做的。人们经常运行的大多数真实的的程序都不是玩具,它们在其他一些函数中有足够的代码,而不是内联到main中。虽然如果启发式算法更聪明一点,如果整个程序或循环中的所有函数都优化为main,那么删除cold就更好了,因为一些真实的程序非常简单。
您可以使用GNUC函数属性覆盖启发式算法。

***__attribute__((hot)) int main(){ ...**优化您期望的方式

(来自Sopel评论的Godbolt,添加了属性)。

  • __attribute__((cold))在一个不叫main的函数上产生idiv
  • __attribute__((optimize("O3")))没有帮助。

int main(int x, char **y){ return x/2; }does 仍然使用gcc -O2的移位,所以main being cold并不总是有这种效果(不像-Os)。
但是也许你的除法已经是有条件的了,GCC猜测基本块甚至不会每次都运行,所以这是更有理由让它变小而不是变快的原因。
疯狂的是,GCC -Os for x86-64(Godbolt)确实使用idiv来进行常数2的有符号除法,而不仅仅是用于任意常数(GCC通常使用uses a multiplicative inverse,即使在-O0上也是如此)。如果任何代码大小与一个算术右移,带有一个fixup以向零舍入(而不是-inf),并且可能会慢得多,特别是对于Ice Lake之前的Intel上的64位整数。AArch 64也是如此,它是2个固定大小的指令,sdiv几乎肯定要慢得多。
sdiv确实在AArch 64上保存了一些代码大小,以获得更高的2次幂(Godbolt),但仍然慢得多,因此对于-Os来说可能不是一个好的折衷方案。idiv不保存x86-64上的指令(因为需要将cdqcqo保存到RDX中),尽管可能有几个字节的代码大小。因此,可能只适用于-Oz,它还将使用push 2/pop rcx将一个小常数放入3 bytes of x86-64 machine code instead of 5中的寄存器。

相关问题