如何在x86-64中最有效地设置和清除零标志(ZF)?
最好使用不需要已知值的寄存器或根本不需要任何空闲寄存器的方法,但如果在这些或其他假设成立时存在更好的方法,则也值得一提。
如何在x86-64中最有效地设置和清除零标志(ZF)?
最好使用不需要已知值的寄存器或根本不需要任何空闲寄存器的方法,但如果在这些或其他假设成立时存在更好的方法,则也值得一提。
这更加困难。对于两个已知不相等的寄存器进行cmp
操作或者使用任何一种值使得某个寄存器显然不可能拥有的cmp reg, imm
操作,例如使用cmp reg,1
来与任何已知为零的寄存器进行比较。
通常情况下,使用test reg, reg
与已知非零寄存器值(例如指针)是一个好选择。
test rsp, rsp
也是一个不错的选择,或者甚至可以使用test esp, esp
以节省一个字节,除非你的栈位于跨越4G边界的异常位置。
我没有看到任何一种方法可以在一条指令中创建ZF=0而不依赖于某些输入寄存器的虚假依赖。如果你不介意破坏某个寄存器并打破虚假依赖,则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=未定义)。
如果你需要在循环中执行此操作,显然可以在循环外部为其进行准备,如果你可以将一个寄存器专门用于配合test
指令的非零值。
最少破坏性: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相同。
缺点:
仅供娱乐,其他同样便宜的替代方案包括test al,0
。对于AL是2个字节,对于任何其他8位寄存器是3或4个字节。 (REX) + opcode + modrm + imm8。原始寄存器值无关紧要,因为零保证reg&0=0
。
如果您恰好在一个寄存器中具有1
或-1
,可以破坏32位模式的寄存器,则inc
或dec
仅使用1个字节即可设置ZF。但在x86-64中,至少需要2个字节。对于64位模式中实际高效并设置FLAGS的1字节指令没有想法。
sbb same,same
可以设置 ZF=!CF (保持 CF 不变), 并将寄存器设置为 0 (CF=0) 或 -1 (CF=1). 在 AMD 的 Bulldozer (BD-family 和 Zen-family) 之后, 这个操作不依赖于 GP 寄存器,只依赖于 CF. 但是在其他微架构上没有特殊处理,并且存在关于寄存器的错误依赖。在 Broadwell 之前的 Intel 上,它需要 2 个微操作。
要设置 ZF=!integer_reg,普通的 test reg,reg
是最好的选择。(比 and reg,reg
或 or reg,reg
更好,除非您有意更改寄存器以避免 P6 寄存器读取停顿。)
如果该寄存器值为零,则 ZF=1,因此与 C 的逻辑反转运算符类似。
可能使用 setz al
/ test al, al
. 没有单个指令: 我不认为有任何读取 ZF 并写入 FLAGS 的指令。 setz
将 ZF 实现在一个寄存器中,然后 test
只是 ZF = !reg
。
pushf
/pop rax
不太可取,但使用 popf
写入标志非常慢 (例如在 SKL 上的吞吐量为 1/20c)。它是微代码化的,因为像 IF 这样的标志也存在于 EFLAGS 中,并且不存在仅条件码版本或用户空间的特殊快速路径。(或者也许 20c 是快速路径。)lahf
(FLAGS->AH) / sahf
(AH->FLAGS) 可以很有用,但会忽略 OF。CF 具有 clc
/stc
/cmc
指令。(clc
在 SnB-family 上与 xor-清零一样有效率。)
LAHF
/SAHF
指令,并将其传输到/从AH寄存器中,在此寄存器上可以应用任何位操作。 LAHF ; Load lower 8 bit from Flags into AH
AND AH,010111111b ; Clear bit for ZF
SAHF ; Store AH back to Flags
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的标志(在汇编代码中)?所述。
一些非常早期的x86-64 CPU,尤其是所有
CPUID
并测试2^0 = 1来检查它。cmp eax, eax
sbb reg, reg
会设置ZF = !CF。因为CF = 1会导致FFFFh,CY,NZ,而CF = 0会导致0000h,NC,ZR。 - ecmZF = !ZF
(类似于CMC
),有没有一种“合理”的方法可以实现? - 640KBsetz al
/test al, al
。 - Peter Cordes