assembly 为什么在SETcc之前进行XOR?

flvtvl50  于 2023-10-19  发布在  其他
关注(0)|答案(1)|浏览(94)

这段代码

int foo(int a, int b)
{ 
    return (a == b);
}

生成以下程序集(https://godbolt.org/z/fWsM1zo6q

foo(int, int):
        xorl    %eax, %eax
        cmpl    %esi, %edi
        sete    %al
        ret

根据https://www.felixcloutier.com/x86/setcc
[SETcc]根据状态标志的设置,将目标操作数设置为0或1
那么,如果a == b的结果是0/1,那么通过先执行xorl %eax, %eax来初始化%eax的意义是什么呢?gcc和clang由于某种原因都无法避免的CPU时钟浪费,这不是吗?

piwo6bdm

piwo6bdm1#

因为setcc很烂:仅在8位操作数大小中可用。但是您使用了32位int作为返回值,因此您需要将8位结果零扩展为32位。
即使你只想返回一个boolchar,你仍然可以对avoid a false dependency when writing AL这样做。异或归零并不花费“一个周期”,它花费1 uop(和英特尔的nop一样便宜),但这仍然不是免费的。(https://agner.org/optimize/
不幸的是,AMD 64没有改变setcc,也没有任何后来的扩展,所以即使使用-march=icelake-clientznver3,在x86上生成32位0/1仍然是一个痛苦。让66操作数大小或rep前缀修改setcc以使用32位操作数大小将有助于避免为此浪费指令(和前端uop),但两家供应商都没有费心引入这样的扩展。(通常只有那些可以在几个“热”函数中提供给予主要加速的扩展,您可以对其进行动态调度,而不是需要在任何地方使用的东西来增加一个小的改进。)
当你有一个备用寄存器时,在setcc之前进行异或置零是最不坏的方法,正如我在x86汇编中将寄存器置零的最佳方法是什么的回答的底部所讨论的那样:xor,mov还是and?.
如果您确实想覆盖比较输入,则其他选项包括:

1. mov-imm 32 =0,您可以在 * 比较后 * 执行,不影响FLAGS:

# for example if you want to replace a compare input with a boolean
    cmp    %ecx, %eax
    mov    $0, %eax
    setcc  %al

这浪费了代码大小(5字节,2对于mov vs. xor)和(on Intel P6-family) has a partial register stall,因为没有使用xor调零来设置内部RAX=AL upper-bytes-known-zero状态。
mov-immediate不在关键路径上,因此乱序exec可以在比较输入准备好之前提前完成,并将置零寄存器准备好供setcc写入。
(On英特尔SnB系列CPU,异或归零是在重命名逻辑中处理的,所以它不必提前执行以准备好零;它在进入后端时就已经完成了。例如,在前端停止之后,XOR置零和SETCC可以在同一周期中进入后端,但是SETCC仍然可以在之后的第一周期中执行,这与如果它是必须实际上在后端执行单元上运行以将零写入寄存器的MOV立即数的情况不同。

2.对8位setcc结果执行MOVZX

cmp    %ecx, %eax
    setcc  %cl
    movzbl %cl, %eax

这在大多数情况下甚至更糟,除了在P6系列上,它避免了部分寄存器停顿。
但是movzx处于从比较输入就绪到0/1结果就绪的关键路径上。(虽然IvyBridge and later can run it with zero latency when it's between two separate registers,这就是为什么我使用%cl而不是%al。编译器通常不会对此进行优化,如果他们不首先设法将某些内容异或为零,他们会优化setcc %al/movzbl %al, %eax。这甚至在具有它的Intel CPU上也击败了移动消除。
setcc %cl在RCX上有a false dependency(除了在Intel P6系列上,它将low 8寄存器与完整寄存器分开重命名),但这没关系,因为RCX和RAX都已经是导致setcc的依赖链的一部分。
如果你不需要一个比较输入,就对单独的目标寄存器进行异或置零。cmp %esi, %edi之后的setcc %al/movzbl %al, %eax将是所有可能选项中最糟糕的,因为RAX可能是在函数调用之前由独立的缓存未命中加载,或缓慢的div或类似的东西写入的,因此您可能会将此依赖关系链耦合到其中。

更新:APX将修复此问题

英特尔的APX扩展(计划用于Granite急流,参见https://www.intel.com/content/www/us/en/developer/articles/technical/advanced-performance-extensions-apx.html手册)将有一个setcc版本,该版本将寄存器的上部置零,因此类似于setcc rax
不幸的是,好的版本需要一个4字节的EVEX前缀;使用2字节的REX 2,您只能获得setcc的现有错误语义。
尽管如此,它完全解决了与xor+setcc类似的机器码大小的问题,更重要的是,在FLAGS设置指令之前不需要任何寄存器设置,因此编译器不必处理它。
因此,也许再过20年或更长时间,如果/当APX成为大多数构建的基准(除了JIT和只需要在当前机器上运行的-march=native构建),我们最终可以基本摆脱从386继承的这个疣,AMD 64拒绝修复。如果AArch 64或RISC-V到那时还没有取代x86-64。(AArch 64有一个非常好的csinc条件选择递增指令,它可以与它的零寄存器一起使用,在单个指令中具体化0或1,或者直接有条件地递增。

相关问题