Udu*_*uru 8 c optimization gcc swap x86-64
在围绕Compiler Explorer进行测试时,我尝试了以下无溢出函数来计算 2 个无符号 32 位整数的平均值:
uint32_t average_1(uint32_t a, uint32_t b)
{
if(a < b){
a ^= b;
b ^= a;
a ^= b;
}
return b + (a - b) / 2;
}
Run Code Online (Sandbox Code Playgroud)
被编译成这样:(与激活的-O1
, -O2
,-O3
优化相同)
average_1:
cmp edi, esi
jnb .L2
mov eax, edi
mov edi, esi
mov esi, eax
.L2:
sub edi, esi
shr edi
lea eax, [rdi+rsi]
ret
Run Code Online (Sandbox Code Playgroud)
其中代码的交换部分经过优化,可以使用mov
具有 1 个附加寄存器的指令。
我已经阅读了这些问题:
并得到:
mov
指令方法需要更多的内存读取,因此不会对执行速度产生负面影响。但在这种情况下,看到的不是内存,而是仅使用了eax
, esi
,寄存器,我认为编译后的汇编代码也可以生成为:edi
average_1:
cmp edi, esi
jnb .L2
xor edi, esi
xor esi, edi
xor edi, esi
...
Run Code Online (Sandbox Code Playgroud)
由于没有内存读取和相同数量的指令,我没有看到任何不良影响,并且对它的更改感到奇怪。显然,有些事情我没有想到,但它是什么?
编辑:要明确的是,这里我的问题不是“为什么不使用 XOR 交换”,而是
“当使用 XOR 交换时,虽然在这种特殊情况下它似乎不会影响执行速度,但为什么它仍然被优化掉” ? ”
Clang 也做同样的事情。可能是出于编译器构造和 CPU 架构的原因:
在某些情况下,将逻辑分解为交换可能会带来更好的优化;对于编译器来说,尽早执行这件事绝对是有意义的,这样它就可以通过交换来跟踪值。
异或交换对于交换寄存器来说完全是垃圾,唯一的优点是它不需要临时。但xchg reg,reg
已经做得更好了。
我对 GCC 的优化器识别异或交换模式并将其解开以遵循原始值并不感到惊讶。一般来说,这使得通过交换实现常量传播和值范围优化成为可能,特别是在交换不以被交换的变量值为条件的情况下。这种模式识别可能在将程序逻辑转换为 GIMPLE(SSA ) 表示后不久发生,因此此时它会忘记原始源曾经使用过异或交换,并且不会考虑以这种方式发出 asm。
希望有时这可以让它优化到只有一个mov
或两个mov
s,具体取决于周围代码的寄存器分配(例如,如果其中一个变量可以移动到新的寄存器,而不必最终返回到原始位置) )。以及这两个变量是否在稍后实际使用,或者仅使用一个。或者,如果它可以完全解开无条件互换,也许不会mov
指示。
但最坏的情况是,mov
需要临时寄存器的三个指令仍然更好,除非寄存器用完。我猜 GCC 不够智能,无法使用xchg reg,reg
而不是溢出其他内容或保存/恢复另一个 tmp reg,因此可能存在这种优化实际上会造成伤害的极端情况。
(显然,GCC-Os
确实有一个窥视孔优化来使用,xchg reg,reg
而不是 3x mov:PR 92549已针对 GCC10 进行了修复。它在 RTL -> 汇编期间很晚才查找该优化。是的,它在这里起作用:将 xor-swap 转换为 xchg : https: //godbolt.org/z/zs969xh47)
由于没有内存读取和相同数量的指令,我没有看到任何不良影响,并且对它的更改感到奇怪。显然有些事情我没有想到,但它是什么?
指令数只是与性能分析相关的三件事之一的粗略代理之一的粗略代理:前端微指令、延迟和后端执行端口。(机器代码大小以字节为单位:x86 机器代码指令是可变长度的。)
它的机器代码字节大小相同,前端微指令数量相同,但关键路径延迟更差:例如,异或交换从输入a
到输出a
需要 3 个周期,从输入b
到输出需要 2 个周期。a
MOV-swap 从输入到输出的最差延迟为 1 周期和 2 周期延迟,或者使用mov-elimination时延迟更小。(这也可以避免使用后端执行端口,特别是与 IvyBridge 和 Tiger Lake 等前端比整数 ALU 端口数量宽的 CPU 相关。还有 Ice Lake,除了 Intel 禁用了 mov-elimination 作为勘误表解决方法;不确定是否为 Tiger Lake 重新启用。)
还相关:
xchg reg,reg
只有 2 uops。GCC 真正错过的优化(即使使用-O3
)是尾部重复导致大约相同的静态代码大小,只是几个额外的字节,因为这些大多是 2 字节指令。最大的胜利是a<b
路径变得与另一条路径的长度相同,而不是先进行交换然后运行相同的 3 uops 进行平均的长度的两倍。
更新:GCC 将为您执行此操作-ftracer
(https://godbolt.org/z/es7a3bEPv),优化交换。 (这只能手动启用或作为 的一部分启用-fprofile-use
,而不是在 处启用-O3
,因此在没有 PGO 的情况下一直使用可能不是一个好主意,可能会在冷函数/代码路径中使机器代码膨胀。)
在源代码中手动执行此操作(Godbolt):
uint32_t average_1_taildup(uint32_t a, uint32_t b)
{
if(a < b){
return a + (b - a) / 2;
}else {
return b + (a - b) / 2;
}
}
Run Code Online (Sandbox Code Playgroud)
# GCC11.2 -O3
average_1_taildup:
cmp edi, esi
jnb .L5
sub esi, edi
shr esi
lea eax, [rsi+rdi]
ret
.L5:
sub edi, esi
shr edi
lea eax, [rdi+rsi]
ret
Run Code Online (Sandbox Code Playgroud)
Clang使用(例如 cmp / mov / cmovb / cmovb,或者使尾部重复版本有点混乱)将版本 1 和1_taildup
编译成代码。cmov
但如果你打算无分支机构,那么你的average_3
优势是:
uint32_t average_3(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
Run Code Online (Sandbox Code Playgroud)
# clang 13.0 -O3
average_3:
mov eax, esi
and eax, edi
xor esi, edi
shr esi
add eax, esi
ret
Run Code Online (Sandbox Code Playgroud)
GCC 和 clang 的版本都只有 5 条指令(加上 ret),但 clang 对其进行了安排,因此即使没有消除,从输入到输出的关键路径延迟也只有 3 个周期(3 个单微指令指令)mov
。(GCC 有一条链,长度为 4 条指令,包括一个 mov。)
另请参阅C/C++ 中的高效防溢出算术平均值- 扩大到 uint64_t 可能更便宜,尤其是在 64 位计算机上内联时。(正如问题下的评论中所讨论的,例如https://godbolt.org/z/sz53eEYh9显示了我评论时现有答案中的代码。)
另一个不错的选择是这样,但通常不如加宽好:
return (a&b) + (a^b)/2;
Run Code Online (Sandbox Code Playgroud)
如果编译器识别出这些习惯用法中的任何一个,他们就可以使用 asm add
/rcr
技巧,这比扩大到平均更unsigned __int128
有效uint64_t
。
归档时间: |
|
查看次数: |
843 次 |
最近记录: |