我注意到 Clang 对以下代码片段做了一个有趣的除法优化技巧
int64_t s2(int64_t a, int64_t b)
{
return a/b;
}
如果将
march
指定为 Sandy Bridge 或以上,则以下是装配输出
mov rax, rdi
mov rcx, rdi
or rcx, rsi
shr rcx, 32
je .LBB1_1
cqo
idiv rsi
ret
.LBB1_1:
xor edx, edx
div esi
ret
的Godbolt链接
据我了解,它检查两个操作数的高位是否为零,如果为零则进行 32 位除法
我检查了此表,发现 Core2 和 Nehalem 上 32/64 位除法的延迟分别为 40/116 和 26/89。因此,如果操作数确实通常不宽,那么通过进行 32 位除法而不是 64 位除法所节省的成本可能与 SnB 一样有价值
那么为什么它只针对 SnB 和更高版本的微架构启用呢?为什么其他编译器(例如 GCC 或 ICC)不这样做?
我猜测 clang 开发者测试了它适用于哪些 uarch,并发现它只是 SnB 系列。
这听起来不错,因为 P6 系列上有一个时髦的档位,以及 AMD 不同的分隔符。
在 P6 系列上使用移位 imm8(不是隐式移位 1)的标志结果会导致前端在发出标志读取指令之前停止,直到移位“退休”。 (因为 P6 解码器不会检查 imm8=0 的情况以保持标志未修改,而 SnB 会检查)。 INC 指令与 ADD 1:这重要吗?。 这可能就是 clang 不将它用于 P6 系列的原因。
可能有一种不同的方法来检查不会导致此停顿的相关条件(例如在test rcx,rcx
之前使用
je
,这在 Core2/Nehalem 上是值得的)。但如果 clang 开发人员没有意识到P6 系列上速度缓慢的原因是他们不会考虑修复它,只是没有为 SnB 之前的目标完成它。 (不幸的是,没有人将我添加到有关此问题的补丁审查或 bug CC 列表中;这是我第一次看到 clang 进行此优化。尽管我认为我可能在其他一些 LLVM 审查或评论中提到了移位标志停顿无论如何,尝试添加
test
并看看这是否值得在 Nehalem 上使用可能会很有趣。)
根据 Agner Fog 的说法,无论操作数大小如何,AMD 的除法器都具有相同的最佳情况除法性能,大概仅取决于输入的实际大小。 只有最坏的情况会随着操作数的大小而增长。 认为在AMD上以符号扩展至128/64位的小输入运行idiv r64
是无害的。
(AMD上的div/idiv对于所有操作数大小都是2微指令(除了8位,它是1)因为它只需要写入一个输出寄存器:AH 和 AL = AX,这与 Intel 的微编码整数除法不同。)Ice Lake 之前的英特尔非常
不同:idiv r32
为 9 uops,而
idiv r64
为 59 uops,在 Haswell 上,最佳情况吞吐量要差 3 倍。 SnB 系列的其他成员,直至 Skylake 及其衍生产品(如 Cascade Lake)都是类似的。Ice Lake 添加了一个新的整数除法单元,以与 idiv r64
相同的 uops 数量处理
idiv r32
,因此在该处理器和较新的 Intel 上,对于相同的输入数,可能具有相同的性能,至少对于混合 CPU,如 Alder Lake。 (对于 idiv r64
,其输入可以适合 i64 被除数和 i32 除数)。相关:
可能是因为 Clang 开发者想到了这一点,而 GCC/ICC 还没有复制它们。 如果您看过 Chandler Carruth 关于
perf
的演讲,他使用的一个例子是摆弄一根树枝来跳过
div
。 我猜这个优化是他的主意。 看起来很漂亮。 :)