在x86中设置和清除零标志位

8

如何在x86-64中最有效地设置和清除零标志(ZF)?

最好使用不需要已知值的寄存器或根本不需要任何空闲寄存器的方法,但如果在这些或其他假设成立时存在更好的方法,则也值得一提。

3个回答

12

ZF=0

这更加困难。对于两个已知不相等的寄存器进行cmp操作或者使用任何一种值使得某个寄存器显然不可能拥有的cmp reg, imm操作,例如使用cmp reg,1来与任何已知为零的寄存器进行比较。

通常情况下,使用test reg, reg与已知非零寄存器值(例如指针)是一个好选择。
test rsp, rsp也是一个不错的选择,或者甚至可以使用test esp, esp以节省一个字节,除非你的栈位于跨越4G边界的异常位置。

我没有看到任何一种方法可以在一条指令中创建ZF=0而不依赖于某些输入寄存器的虚假依赖。如果你不介意破坏某个寄存器并打破虚假依赖,则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=未定义)。

如果你需要在循环中执行此操作,显然可以在循环外部为其进行准备,如果你可以将一个寄存器专门用于配合test指令的非零值。


ZF=1

最少破坏性:cmp eax,eax,但可能存在虚假依赖,需要一个后端微操作,并不能实现清零技巧。由于RSP通常不会发生太多更改,因此cmp esp, esp可能是一个不错的选择。(除非这会强制进行堆栈同步微操作)。

最有效的方法:异或清零(例如使用任何一个可用寄存器的xor eax, eax)绝对是SnB系列上最高效的方法之一(与2字节nop的成本相同,如果需要使用REX,则为3字节,因为你想将r8d..r15d中的一个置零):1个前端微操作,在SnB系列上没有后端微操作,FLAGS结果在发出同一周期内准备好。(仅在前端停滞或其他情况下有用,在这些情况下,依赖于它的微操作会在同一周期发出,并且RS中没有准备好输入的较旧的微操作,否则,这样的微操作会优先于任何执行端口。)

标志位结果: ZF=1 PF=1 SF=0 CF=0 OF=0 (AF=未定义)。 (或使用sub eax,eax来获得定义良好的AF=0。在实践中,现代CPU也会选择将AF设置为0,以便以相同的方式解码两种零化习语。Silvermont只将32位操作数大小的XOR识别为零化惯用语,而不是SUB。)

xor-zero在所有其他uarch上都非常便宜:没有输入依赖关系,并且不需要任何预先存在的寄存器值。(因此不会导致P6家族的寄存器读取停顿)。所以,在任何其他uarch上,它最多与您可以在该uarch上执行的任何其他操作相匹配(其中它确实需要一个执行单元)。

(在 Pentium M之前的早期P6家族中,xor -零化不会打破依赖性;它仅触发避免部分寄存器问题的特殊al=eax状态。但是这些CPU都不是x86-64,而是仅限32位。)

通常希望为某些事情获取一个清零的寄存器,例如作为sub目标的0-x进行复制和取反,因此可以将xor-zero放在需要它的地方,以创建有用的FLAG条件。

有趣但可能不太有用的是:test al,0长为2个字节。 但是cmp esp,esp也是这样。


正如@prl所建议的那样,使用任何寄存器的cmp same,same将适用而不会干扰值。我怀疑这不是像sub same,same 那样特殊处理为打破依赖性的CPU,因此选择一个“冷”寄存器。再次是2或3个字节,1个uop。它可以与JCC微融合,但这很愚蠢(除非JCC也是从其他条件到达的分支目标?)

标志位结果:与xor-zeroing相同。

缺点:

  • (可能)存在假依赖关系
  • 在P6家族中可能会导致寄存器读取停顿,因此要选择您已经在附近指令中正在读取的冷寄存器。
  • 在SnB-family中需要后端执行单元。

仅供娱乐,其他同样便宜的替代方案包括test al,0。对于AL是2个字节,对于任何其他8位寄存器是3或4个字节。 (REX) + opcode + modrm + imm8。原始寄存器值无关紧要,因为零保证reg&0=0


如果您恰好在一个寄存器中具有1-1,可以破坏32位模式的寄存器,则incdec仅使用1个字节即可设置ZF。但在x86-64中,至少需要2个字节。对于64位模式中实际高效并设置FLAGS的1字节指令没有想法。


ZF=!CF

sbb same,same 可以设置 ZF=!CF (保持 CF 不变), 并将寄存器设置为 0 (CF=0) 或 -1 (CF=1). 在 AMD 的 Bulldozer (BD-family 和 Zen-family) 之后, 这个操作不依赖于 GP 寄存器,只依赖于 CF. 但是在其他微架构上没有特殊处理,并且存在关于寄存器的错误依赖。在 Broadwell 之前的 Intel 上,它需要 2 个微操作。


ZF=!bool(integer register)

要设置 ZF=!integer_reg,普通的 test reg,reg 是最好的选择。(比 and reg,regor reg,reg 更好,除非您有意更改寄存器以避免 P6 寄存器读取停顿。)

如果该寄存器值为零,则 ZF=1,因此与 C 的逻辑反转运算符类似。

ZF=!ZF

可能使用 setz al / test al, al. 没有单个指令: 我不认为有任何读取 ZF 并写入 FLAGS 的指令。 setz 将 ZF 实现在一个寄存器中,然后 test 只是 ZF = !reg


其他 FLAGS 条件:

CF 具有 clc/stc/cmc 指令。(clc 在 SnB-family 上与 xor-清零一样有效率。)


我相信sbb reg, reg会设置ZF = !CF。因为CF = 1会导致FFFFh,CY,NZ,而CF = 0会导致0000h,NC,ZR。 - ecm
1
@ecm:好的,谢谢,已修复。我最初可能在思考时混淆了register=0但ZF=1。 - Peter Cordes
关于补充/反转 ZF = !ZF(类似于 CMC),有没有一种“合理”的方法可以实现? - 640KB
2
@640KB: 可能不是单个指令,但也许可以使用 setz al / test al, al - Peter Cordes

3
最不具干扰性的操作控制Flags低8位中的任意一位,是使用经典的LAHF/SAHF指令,并将其传输到/从AH寄存器中,在此寄存器上可以应用任何位操作。
(i) 仅针对第7 (SF)、第6 (ZF)、第4 (AF)、第2 (PF)和第0 (CF)位。

关闭ZF

       LAHF                      ; Load lower 8 bit from Flags into AH
       AND      AH,010111111b    ; Clear bit for ZF
       SAHF                      ; Store AH back to Flags

打开ZF

       LAHF                      ; Load AH from FLAGS
       OR       AH,001000000b    ; Set bit for ZF
       SAHF                      ; Store AH back to Flags

当然,任何CMP (E)AX,(E)AX都可以更快地设置ZF并用更少的代码; 这样做的重点是保持其他FLAGS不变,就像如何直接读写x86标志寄存器?如何手动更改8086的标志(在汇编代码中)?所述。

早期AMD64的注意事项 - 长模式下的LAHF是一种扩展

一些非常早期的x86-64 CPU,尤其是所有

  • AMD Athlon 64,Opteron和Turion 64 D版之前 (2005年3月)和
  • Pentium 4步进G1之前的Intel *(2005年12月)
由于那条指令最初被从AMD64指令子集中移除,但后来又被重新引入。幸运的是,在x86-64成为常见景象之前,这种情况已经发生了,因此只有少数早期高端CPU受到影响,而且即使是今天也很少有存活的。特别是这些CPU无法运行Windows 10,或者在Windows 10之前运行任何64位Windows(请参见SuperUser.SE上的这个答案)。如果您真的希望有人尝试在超过17年的高端CPU上运行该软件,可以通过使用EAX = 80000001h执行CPUID并测试2^0 = 1来检查它。

2
假设您不需要保留其他标志的值,最初的回答是:
cmp eax, eax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接