在轮班操作后使用携带标志

Using carry flag after shift operation

本文关键字:标志 操作      更新时间:2023-10-16

对于以下代码的div/mod 部分:

int pow(int x, unsigned int n)
{
int y = 1;
while (n > 1)
{
auto m = n%2;
n = n/2;
if (m)
y *= x;
x = x*x;
}
return x*y;
}

我希望组装像

shr n
cmovc y, yx

但是 gcc/clang 甚至 icc 在这里不使用进位标志(使用 2 个寄存器和 and/test 代替(:https://godbolt.org/z/L6VUZ1

所以我想知道如果你手动编码它的最佳方法是什么以及为什么(ILP,依赖项,...(。

test/je可以在主流 Intel 和 AMD CPU 上宏融合成单个 uop,因此在分支代码中,您只会通过使用班次的 CF 输出而不是更早的test/je来节省代码大小并花费 1 个周期的分支错误检测延迟。

(不幸的是,gcc 在这里真的很愚蠢,并且在and edx,1的结果上使用test edx,edx,而不仅仅是使用test dl,1或更好的test sil,1来保存movtest esi,1将使用imm32编码,因为test没有test r/m32, imm8编码,所以编译器知道读取窄寄存器以获得test

但是在像 clang 使用的无分支实现中,是的,您可以通过使用 CF 输出来保存 uopcmovc而不是单独计算输入以进行cmovetest。 你仍然没有缩短关键路径,因为testshr可以并行运行,而像 Haswell 或 Ryzen 这样的主流 CPU 拥有足够宽的管道来充分利用所有 ILP,只是imul循环承载依赖链上的瓶颈。 (https://agner.org/optimize/(。

实际上,这是cmov->imul->下一个迭代 dep 链y这是瓶颈。 在 Haswell 及更早版本上,cmov是 2 个周期延迟 (2 uops(,因此总 dep 链为 2+3 = 5 个周期。 (流水线乘法器意味着做额外的y*=1乘法不会减慢x*=x部分的速度,反之亦然;它们可以同时在飞行中,只是不会在同一周期中开始。

如果你对不同的碱基重复使用相同的n,分支版本应该预测得很好,并且非常好,因为分支预测 + 推测执行将数据依赖链解耦。

否则,最好吃掉无分支版本的更长延迟,而不是遭受分支未命中。