【问题标题】:Why XOR before SETcc?为什么在 SETcc 之前进行异或?
【发布时间】:2021-09-03 03:36:14
【问题描述】:

这段代码

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 的结果将为零/一,那么首先通过执行xorl %eax, %eax%eax 初始化为零有什么意义呢?这不是 gcc 和 clang 出于某种原因都无法避免的 CPU 时钟的浪费吗?

【问题讨论】:

  • setcc 只对一个字节进行操作,在本例中为al。返回值为 32 位,因此高位需要清零。
  • 在调用(内联)时查看foo - godbolt.org/z/sGqe1WT9o - 使用extern int 来阻止优化器完全替换代码。
  • 另请参阅stackoverflow.com/a/33668295/15416 进行深入了解

标签: c++ assembly x86 micro-optimization


【解决方案1】:

因为setcc 很烂:仅适用于 8 位操作数大小。但是您使用 32 位 int 作为返回值,因此您需要将 8 位结果零扩展为 32 位。

即使您确实只想返回boolchar,您仍然可以对avoid a false dependency when writing AL 执行此操作。 xor-zeroing 不花费“一个周期”,它花费 1 uop(并且与 Intel 的 nop 一样便宜),但这仍然不是免费的。 (https://agner.org/optimize/)

不幸的是,AMD64 没有更改 setcc,也没有更改任何后续扩展,因此即使使用 -march=icelake-clientznver3,在 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 或类似的东西调用,因此您可以将此依赖链耦合到其中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-06
    • 2010-12-02
    • 1970-01-01
    • 1970-01-01
    • 2021-07-16
    • 1970-01-01
    • 2015-12-27
    相关资源
    最近更新 更多