为什么 gcc 12.2 没有优化从 main() 调用的这个 constexpr 函数中的移位划分



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

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

代码有点直截了当。这里重要的部分是 2 内func(int x)的最终除法。由于 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

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

GCC 特别对待main:隐式__attribute__((cold))

因此,main优化较少(或偏爱大小而不是速度),因为它在大多数程序中通常只调用一次。__attribute__((cold))-Os(优化大小)并不完全相同,但它是朝着这个方向迈出的一步,有时会获得成本启发式来选择朴素的除法指令。

正如GCC开发人员Marc Glisse评论的那样,如果你正在对代码进行基准测试或查看它如何优化,请不要将你的代码放在一个名为main的函数中。(除了cold之外,还可以有其他特殊的东西,例如MinGW GCC为init函数增加了额外的callgcc -m32添加代码以使堆栈对齐16。 所有这些都是你不想要的噪音,因为你正在查看的代码。 参见 如何从 GCC/clang 程序集输出中去除"噪音"?

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

对于所有代码都在main的玩具程序来说,这是一个糟糕的启发式方法,但这就是GCC所做的。 人们经常运行的大多数真实程序都不是玩具,并且在其他函数中有足够的代码,以至于它不会内联到main中。 尽管如果启发式算法更聪明一点并删除cold会很好,如果事实证明整个程序或循环中的所有功能确实优化为main,因为一些真正的程序非常简单。

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

  • __attribute__((hot)) int main(){ ...优化了你期望的方式
    (来自Sopel评论的Godbolt,添加了属性)。
  • __attribute__((cold))不调用的函数main会产生idiv
  • __attribute__((optimize("O3")))无济于事。

int main(int x, char **y){ return x/2; }仍然使用带gcc -O2的班次,所以主要cold并不总是有这种效果(不像-Os)。

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


疯狂的是,x86-64 的 GCC-Os(Godbolt)确实使用idiv表示有符号除法常量 2,而不仅仅是任意常量(GCC 通常即使在-O0时使用乘法逆)。 如果有任何代码大小与算术右移相比,它不会节省太多,修复为四舍五入到零(而不是 -inf),并且可能会慢得多,尤其是对于 Ice Lake 之前的英特尔上的 64 位整数。 AArch64 也是如此,无论哪种方式,它都是 2 条固定大小的指令,sdiv几乎可以肯定要慢得多。

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

最新更新