在 x86 中设置和清除零标志

Setting and clearing the zero flag in x86

在 x86-64 中设置和清除零标志 (ZF) 的最有效方法是什么?

不需要具有已知值的寄存器,或者根本不需要任何空闲寄存器的方法是首选,但如果在这些或其他假设为真时有更好的方法可用,也值得一提。

假设您不需要保留其他标志的值,

cmp eax, eax

ZF=0

这更难。 cmp 在任何两个已知不相等的 regs 之间。或者 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 eaxdec 如果您不介意破坏寄存器,打破错误的依赖关系,将在 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.


ZF=1

破坏性最小:cmp eax,eax - 但具有错误的依赖性(我假设)并且需要后端 uop:不是归零惯用语。 RSP 通常变化不大,因此 cmp esp, esp 可能是一个不错的选择。 (除非强制堆栈同步 uop)。

最有效:(如 xor eax,eax 使用任何免费寄存器)绝对是 SnB 系列上最有效的方法(与 2 字节 [= 的成本相同) 28=],如果需要 REX,则为 3 字节,因为您想将 r8d..r15d 中的一个清零):1 个前端微指令, 零后端微指令 在 SnB 系列上,并且 FLAGS 结果在它发布的同一个周期中准备就绪。 (仅在前端停滞的情况下相关,或者在同一周期中依赖于它的微指令发出并且 RS 中没有任何具有就绪输入的旧微指令的其他情况,否则此类微指令将优先于任何一个执行端口。)

标记结果:ZF=1 PF=1 SF=0 CF=0 OF=0 (AF=undefined).(或者用sub eax,eax搞定-defined AF=0。在实践中,现代 CPU 也选择 AF=0 进行异或归零,因此它们可以以相同的方式解码两个归零习语。Silvermont 仅将 32 位操作数大小的 xor 识别为归零习语,而不是 sub。 )

xor-zero 在所有其他 uarches 上也非常便宜,当然:没有输入依赖性,也不需要任何预先存在的寄存器值。 (因此不会导致 P6 系列寄存器读取停顿)。所以它最坏的情况是与你可以在任何其他 uarch 上做的任何其他事情联系在一起(它确实需要一个执行单元。)

(在早期的 P6 系列上,在奔腾 M 之前,xor-归零 不会 打破依赖关系;它只会触发特殊的 al=eax 状态,避免部分-注册东西。但是 none 这些 CPU 是 x86-64,全部都是 32 位的。)

无论如何,想要一个归零的寄存器是很常见的,例如作为 0 - x 复制和否定的 sub 目的地,因此通过将异或归零放在您需要的地方以创建有用的 FLAG 条件来利用它。

有趣但可能没有用:test al, 0 是 2 个字节长。但 cmp esp,esp.

也是

正如@prl 所建议的,cmp same,same 与任何寄存器都可以工作而不会干扰值 。我怀疑这是 不是 特例,因为依赖打破了 sub same,same 在某些 CPU 上的方式,所以 选择一个“冷”寄存器 .同样是 2 或 3 个字节,1 uop。它可以与 JCC 微融合,但那将是愚蠢的(除非 JCC 也是来自其他条件的分支目标?)

标记结果:与异或归零相同。

缺点:

  • (可能)虚假依赖
  • 在 P6 系列上可能会导致寄存器读取停顿,因此请在附近的说明中选择您已经在读取的冷寄存器。
  • 在 SnB 系列上需要一个后端执行单元

只是为了好玩,其他便宜的替代品包括 test al, 0。 2 个字节用于 AL,3 或 4 个字节用于任何其他 8 位寄存器。 (REX) + 操作码 + modrm + imm8。原始寄存器值无关紧要,因为 imm8 为零可以保证 reg & 0 = 0.


如果你碰巧在寄存器中有一个1-1你可以销毁,32位模式incdec只会将ZF设置为1字节。但在 x86-64 中,这至少是 2 个字节。对于 64 位模式下实际上有效并设置 FLAGS 的 1 字节指令,我没有想到。


ZF=!CF

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=bool(整数寄存器)

设置ZF=integer_reg,显然正常。 (优于 and reg,regor reg,reg,除非您有意重写寄存器以避免 P6 寄存器读取停顿。)


其他FLAGS条件:

  • How to read and write x86 flags registers directly?
  • One instruction to clear PF (Parity Flag) -- get odd number of bits in result register(没有预先存在的 testcmp 的寄存器值是不可能的)。
  • (例如,用于 ADOX 链的开始。)
  • pushf/pop rax 并不可怕,但是用 popf 写标志非常慢(例如 SKL 上的 1/20c 吞吐量)。它是微编码的,因为像 IF 这样的标志也存在于 EFLAGS 中,并且没有仅条件代码版本或用户-space 的特殊快速路径。 (或者 20c 快速路径。)
  • lahf (FLAGS->AH) / sahf (AH->FLAGS) 可能有用但错过了 OF.

CF 有 clc/stc/cmc 指令。 (clc 与 SnB 系列上的异或归零一样有效。)