我想记住编译器如何执行整数除以 2。 有趣的是,我发现了一个我不理解的特殊行为:
https://godbolt.org/z/87n3x5Gjv
int div2(int x)
{
return x / 2;
}
x86_64 gcc 13.2 -O1
div2(int):
mov eax, edi
shr eax, 31
lea eax, [rax+rdi]
sar eax
ret
我个人会将
lea
替换为 add
,-O0 和 -O2 可以这样做,clang 在任何级别也可以这样做。
这与 add
指令可能修改的标志有关系吗?
我很想知道为什么会发生这样的事情,提前谢谢!
我认为 LEA 在这里更糟糕,可能是一些启发式错误的结果。您可以在 GCC 的 bugzilla 上使用“missed optimization”关键字来报告它,https://gcc.gnu.org/bugzilla/
我也注意到这一点,一些编译器似乎无缘无故地更喜欢 LEA,即使它们不需要将结果复制到不是任何输入的目的地。
LEA 在某些 CPU 上运行的端口较少,例如 Ice Lake 之前的 Intel。至少它在所有现代 CPU 上仍然是一个“简单”LEA,有 2 个组件,并且索引上没有比例因子。
LEA 在寻址模式下确实会为 SIB 字节花费额外的代码大小。 (操作码 + ModRM + SIB 用于 3 字节
lea
,仅操作码 + ModRM 用于 2 字节add eax, edi
)
不写 FLAGS 在这里没有用。 x86 CPU 完全高效地处理 FLAGS 写入,甚至对 FLAGS 和整数结果使用相同的物理寄存器文件条目。 (Sandybridge-family 肯定会这样做;我认为 Zen 和其他人也这样做,而不是需要另一个完整的寄存器文件和寄存器分配表或其他东西,或者需要 uops 有 2 个输出。)一些 GCC 开发人员可能对此不确定:我似乎记得对 GCC bugzilla 问题的评论表明,不修改 FLAGS 会有一些优势,比如 CPU 的工作量更少,而不必重命名它。事实并非如此。
当您已经写入寄存器目标时,写入 FLAGS 的额外成本为零。它甚至可能允许释放仅保存标志结果但不保存任何整数寄存器的当前值的旧物理寄存器。