Joh*_*nck 5 c++ optimization gcc x86-64 compiler-optimization
考虑这个 C++ 代码:
#include <cstdint>
// returns a if less than b or if b is INT32_MIN
int32_t special_min(int32_t a, int32_t b)
{
return a < b || b == INT32_MIN ? a : b;
}
Run Code Online (Sandbox Code Playgroud)
GCC-fwrapv正确地认识到减去 1b可以消除特殊情况,并为 x86-64 生成以下代码:
lea edx, [rsi-1]
mov eax, edi
cmp edi, edx
cmovg eax, esi
ret
Run Code Online (Sandbox Code Playgroud)
但如果没有-fwrapv它,会生成更糟糕的代码:
mov eax, esi
cmp edi, esi
jl .L4
cmp esi, -2147483648
je .L4
ret
.L4:
mov eax, edi
ret
Run Code Online (Sandbox Code Playgroud)
我知道-fwrapv如果我编写依赖于签名溢出的 C++ 代码,这是需要的。但:
如果我编写了“手动优化”的 C++ 代码来尝试实现该优化,我知道这-fwrapv是必需的,否则编译器可能会确定有符号溢出是 UB 并在b == INT32_MIN. 但这里编译器处于控制之中,我看不出是什么阻止它使用没有-fwrapv. 有什么理由不允许吗?
这种错过的优化以前在 GCC 中发生过,比如没有完全将有符号 int add 视为关联,即使它正在编译带有包装加法的 2 的补码目标。所以它对无符号优化得更好。IIRC,原因是 GCC 失去了一些有关运营的信息,从而变得保守?我忘记了这个问题是否得到解决。
我找不到以前在 SO 上看到过的地方,以及 GCC 开发人员关于内部结构的回复;也许是在 GCC 错误报告中?我认为这是通过a+b+c+d+e(而不是)重新关联到依赖树来缩短关键路径之类的事情。但不幸的是它仍然存在于当前的 GCC 中:
int sum(int a, int b, int c, int d, int e, int f) {
return a+b+c+d+e+f;
// gcc and clang make one stupid dep chain
}
int sumv2(int a, int b, int c, int d, int e, int f) {
return (a+b)+(c+d)+(e+f);
// clang pessimizes this back to 1 chain, GCC doesn't
}
unsigned sumu(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) {
return a+b+c+d+e+f;
// gcc and clang make one stupid dep chain
}
unsigned sumuv2(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) {
return (a+b)+(c+d)+(e+f);
// GCC and clang pessimize back to 1 chain for unsigned
}
Run Code Online (Sandbox Code Playgroud)
Godbolt for x86-64 System V at-O3、 clang 并gcc -fwrapv为所有 4 个函数制作相同的 asm,如您所料。
GCC(不带)为 和for生成-fwrapv相同的汇编(求和为,保存 的寄存器。) 但是 GCC 为和生成不同的汇编,因为它们使用带符号的sumusumuv2r8desumsumv2int
# gcc -O3 *without* -fwrapv
# The same order of order of operations as the C source
sum(int, int, int, int, int, int):
add edi, esi # a += b
add edi, edx # ((a+b) + c) ...
add edi, ecx # sum everything into EDI
add edi, r8d
lea eax, [rdi+r9]
ret
# also as written, the source order of operations:
sumv2(int, int, int, int, int, int):
add edi, esi # a+=b
add edx, ecx # c+=d
add r8d, r9d # e+=f
add edi, edx # a += c
lea eax, [rdi+r8] # retval = a + e
ret
Run Code Online (Sandbox Code Playgroud)
因此具有讽刺意味的是,当 GCC 不重新关联源时,它会生成更好的汇编。这是假设所有 6 个输入同时准备就绪。如果早期代码的无序执行仅在每个周期产生输入寄存器 1,则此处的最终结果将在最终输入准备好后仅 1 个周期准备就绪(假设最终输入为 )f。
但如果最后一个输入是aor b,则结果要等到 5 个周期后才能准备好,并且尽可能使用像 GCC 和 clang 这样的单链。与树缩减的 3 个周期最坏情况相比,2 个周期最好情况(如果e或f最后准备好)。
(更新:-mtune=znver2使 GCC 重新关联到一棵树中,谢谢@amonakov。所以这是一个默认的调整选择,对我来说似乎很奇怪,至少对于这个特定的问题大小。请参阅GCC 源代码,搜索reassoc以查看成本其他调整设置;其中大多数都是1,1,1,1疯狂的,特别是对于浮点。这可能就是为什么 GCC 在展开 FP 循环时未能使用多个向量累加器,从而达不到目的。)
int但无论如何,这是 GCC 仅与重新关联签名的情况-fwrapv。 很明显,它对自己的限制超出了必要的限度,没有-fwrapv。
相关:编译器优化可能会导致整数溢出。可以吗?- 这当然是合法的,不这样做就是错过优化。
GCC 并没有完全被签名所束缚int;它会自动矢量化int sum += arr[i],并且它确实能够优化为什么GCC不将a*a*a*a*a*a优化为(a*a*a)*(a*a*a)?为签署int a.