因为setcc 很烂:仅适用于 8 位操作数大小。但是您使用 32 位 int 作为返回值,因此您需要将 8 位结果零扩展为 32 位。
即使您确实只想返回bool 或char,您仍然可以对avoid a false dependency when writing AL 执行此操作。 xor-zeroing 不花费“一个周期”,它花费 1 uop(并且与 Intel 的 nop 一样便宜),但这仍然不是免费的。 (https://agner.org/optimize/)
不幸的是,AMD64 没有更改 setcc,也没有更改任何后续扩展,因此即使使用 -march=icelake-client 或 znver3,在 x86 上生成 32 位 0/1 仍然很痛苦。拥有 66 操作数大小或 rep 前缀修改 setcc 以使用 32 位操作数大小将有助于避免为此浪费指令(和前端 uop),但两家供应商都没有打扰过引入这样的扩展。 (通常只有扩展可以在一些“热门”功能中提供重大加速,您可以对其进行动态调度,而不是需要在任何地方使用才能增加小的改进。)
在 setcc 之前进行异或归零是最不坏的方法,当您有备用寄存器时,正如我在 What is the best way to set a register to zero in x86 assembly: xor, mov or and? 的答案底部所讨论的那样。
如果您确实想覆盖比较输入,其他选项包括:
1. mov-imm32=0 您可以在比较之后进行,不影响 FLAGS:
# for example if you want to replace a compare input with a boolean
cmp %ecx, %eax
mov $0, %eax
setcc %al
这会浪费代码大小(5 字节,而 mov 与 xor 为 2),并且在读取 EAX 时(on Intel P6-family) has a partial register stall,因为没有使用 xor-zeroing 来设置内部 RAX=AL upper-bytes-known-zero状态。
mov-immediate 不在关键路径上,因此乱序 exec 可以在比较输入准备好之前尽早完成它,并准备好那个归零的寄存器供 setcc 写入。
(在 Intel SnB 系列 CPU 上,xor-zeroing 在重命名逻辑中处理,因此它不必提前执行以准备好零;它在进入后端时已经完成。例如,在前端停顿、异或置零和 setcc 可以在同一个周期中进入后端,但是 setcc 仍然可以在之后的第一个周期中执行,这与它必须实际运行的 mov-immediate 不同后端执行单元将零写入寄存器。)
2。 MOVZX 在 8 位 setcc 结果上
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 . 即使在具有 mov-elimination 的 Intel CPU 上,这也击败了它。)
setcc %cl 在 RCX 上有 a false dependency(英特尔 P6 系列除外,它将低 8 寄存器与完整寄存器分开重命名),但这没关系,因为 RCX 和 RAX 都已经是导致 setcc 的依赖链的一部分。
如果您没有覆盖比较输入之一,则对单独的目标寄存器进行异或归零。 setcc %al / movzbl %al, %eax after cmp %esi, %edi 将是所有可能选项中最糟糕的,因为 RAX 可能最后是由独立的缓存未命中负载写入的,或者在函数之前缓慢的 div 或类似的东西调用,因此您可以将此依赖链耦合到其中。