【发布时间】:2019-06-27 04:49:36
【问题描述】:
在 x86-64 中设置和清除零标志 (ZF) 的最有效方法是什么?
不需要具有已知值的寄存器或根本不需要任何空闲寄存器即可工作的方法是首选,但如果在这些或其他假设成立时有更好的方法可用,那么也值得一提。
【问题讨论】:
标签: performance assembly x86 x86-64 micro-optimization
在 x86-64 中设置和清除零标志 (ZF) 的最有效方法是什么?
不需要具有已知值的寄存器或根本不需要任何空闲寄存器即可工作的方法是首选,但如果在这些或其他假设成立时有更好的方法可用,那么也值得一提。
【问题讨论】:
标签: performance assembly x86 x86-64 micro-optimization
假设您不需要保留其他标志的值,
cmp eax, eax
【讨论】:
这更难。 cmp 在已知不相等的任何两个 reg 之间。或 cmp reg,imm 具有某些 reg 不可能具有的任何值。例如cmp reg,1 与任何已知零寄存器。
一般test reg,reg 适用于任何已知的非 0 寄存器值,例如一个指针。test rsp, rsp 可能是一个不错的选择, 甚至test esp, esp 保存一个字节都可以,除非您的堆栈位于跨越 4G 的异常位置边界。
我没有看到一种方法可以在一条指令中创建 ZF=0 而不会错误地依赖某些输入 reg。 xor eax,eax / inc eax 或 dec 如果您不介意破坏寄存器,破坏错误的依赖关系,将在 2 微秒内完成任务。 (not 不设置 FLAGS,neg 只会设置 0-0 = 0。)
or eax, -1 不需要寄存器值的任何前置条件。(虚假依赖,但不是真正的依赖,因此即使它可能为零,您也可以选择任何寄存器。)它不必一定是-1,它不会给你带来任何好处,所以如果你能把它变成有用的东西就更好了。
or eax,-1 FLAG 结果:ZF=0 PF=1 SF=1 CF=0 OF=0 (AF=undefined)。
如果您需要在循环中执行此操作,您显然可以在循环之外设置它,如果您可以将一个寄存器专用于非零以用于test。
破坏性最小:cmp eax,eax - 但有一个错误的依赖项(我假设)并且需要一个后端 uop:而不是归零习语。 RSP 通常不会发生太大变化,因此cmp esp, esp 可能是一个不错的选择。 (除非强制堆栈同步 uop)。
最高效:xor-zeroing(如xor eax,eax使用任何空闲寄存器)绝对是SnB系列上最有效的方式(与2字节nop相同的成本,或3 -byte,如果它需要一个 REX,因为你想将 r8d..r15d 中的一个归零):1 个前端 uop,zero back-end uops 在 SnB 系列上,并且 FLAGS 结果已准备好它发出的同一个周期。 (仅在前端停止的情况下相关,或者在其他情况下,取决于它的 uop 在同一周期内发出,并且 RS 中没有任何较旧的 uop 具有准备好的输入,否则此类 uop 将具有优先权执行端口。)
标记结果:ZF=1 PF=1 SF=0 CF=0 OF=0 (AF=undefined)。 (或使用sub eax,eax 得到明确定义的AF=0。在实践现代 CPU 也选择 AF=0 进行异或归零,因此它们可以以相同的方式解码两个归零惯用语。Silvermont 仅将 32 位操作数大小的异或识别为归零惯用语,而不是 sub。)
xor-zero 在所有其他 uarches 上也非常便宜,当然:没有输入依赖项,也不需要任何预先存在的寄存器值。 (因此不会导致 P6 系列寄存器读取停顿)。因此,它最多只能与您可以在任何其他 uarch 上执行的任何其他操作联系在一起(它确实需要一个执行单元。)
(在早期的 P6 系列中,在 Pentium M 之前,xor-zeroing 不会破坏依赖关系;它只会触发特殊的 al=eax 状态,以避免部分注册的东西。但没有一个这些 CPU 是 x86-64,全部都是 32 位的。)
无论如何,想要一个归零的寄存器是很常见的,例如作为sub 的目标0 - x 进行复制和否定,因此通过将异或归零放在您需要的位置来利用它来创建有用的FLAG 条件。
有趣但可能没用:test al, 0 有 2 个字节长。但cmp esp,esp也是如此。
正如@prl 建议的那样,cmp same,same 与任何寄存器都可以在不干扰值的情况下工作。我怀疑这是不是特殊情况,因为依赖关系破坏了sub same,same在某些CPU上的方式,所以选择一个“冷”寄存器。又是 2 或 3 个字节,1 uop。它可以与 JCC 进行微融合,但这很愚蠢(除非 JCC 也是其他条件的分支目标?)
标记结果:与异或归零相同。
缺点:
只是为了好玩,其他便宜的替代品包括test al, 0。 AL 为 2 个字节,任何其他 8 位寄存器为 3 或 4 个字节。 (REX) + 操作码 + modrm + imm8。原始寄存器值无关紧要,因为为零的imm8 保证reg & 0 = 0。
如果您碰巧在寄存器中有1 或-1,您可以销毁,32 位模式inc 或dec 会将ZF 设置为仅1 个字节。但在 x86-64 中,这至少是 2 个字节。对于 64 位模式下的 1 字节指令,没有任何东西会真正有效并设置 FLAGS。
sbb same,same 可以设置 ZF=!CF(不修改 CF),并将 reg 设置为 0 (CF=0) 或 -1 (CF=1)。在自 Bulldozer(BD 系列和 Zen 系列)以来的 AMD 上,这不依赖于 GP 寄存器,仅依赖于 CF。但在其他 uarches 上,它不是特殊情况,并且在 reg 上有一个错误的 dep。在 Broadwell 之前,它是 Intel 的 2 微秒。
设置ZF=integer_reg,显然是正常的test reg,reg is your best bet。 (比and reg,reg 或or reg,reg 好,除非您有意重写寄存器以避免P6 寄存器读取停顿。)
test 或 cmp 的预先存在的寄存器值,则不可能)。pushf/pop rax 并不可怕,但是用popf 写入标志非常慢(例如,SKL 上的吞吐量为 1/20c)。它是微编码的,因为像 IF 这样的标志也存在于 EFLAGS 中,并且没有仅条件代码版本或用于用户空间的特殊快速路径。 (或者也许 20c 是快速路径。)lahf (FLAGS->AH) / sahf (AH->FLAGS) 可能有用,但错过 OF。CF 有clc/stc/cmc 指令。 (clc 与 SnB 系列的异或归零一样有效。)
【讨论】:
sbb reg, reg 设置 ZF = ! CF。由于 CF = 1 导致 FFFFh、CY、NZ 和 CF = 0 导致 0000h、NC、ZR。