在x86-64中,设置和清除零标志(ZF)的最有效方法是什么?不需要具有已知值的寄存器或根本不需要任何空闲寄存器的方法是优选的,但是如果当那些或其它假设为真时有更好的方法可用,则也值得一提。
wmvff8tz1#
ZF=0这是比较困难的。cmp在任何两个已知不相等的寄存器之间。或者cmp reg,imm与一些寄存器不可能具有的任何值。例如,cmp reg,1与任何已知为零的寄存器。
cmp
cmp reg,imm
cmp reg,1
一般来说,test reg,reg适用于任何已知的非0寄存器值,例如指针。**test rsp, rsp可能是一个不错的选择,**甚至test esp, esp都可以保存一个字节,除非您的堆栈位于跨越4G边界的不寻常位置。
test reg,reg
test rsp, rsp
test esp, esp
我看不出有什么方法可以在一条指令中创建ZF=0,而不对某个输入寄存器产生假依赖。如果您不介意破坏一个寄存器,打破假依赖,xor eax,eax/inc eax或dec将在2个微操作中完成此操作。(not不会设置FLAGS,neg只会执行0-0 = 0。)
xor eax,eax
inc eax
dec
not
neg
or eax, -1不需要寄存器值的任何前提条件。(伪相关性,但不是真正的相关性,因此您可以选择任何寄存器,即使它可能为零。)它不一定是-1,它不会为您带来任何好处,因此如果您能使它变得有用,那就更好了。
or eax, -1
-1
or eax,-1
如果需要在循环中执行此操作,显然可以在循环 * 外部 * 对其进行设置,前提是可以指定一个寄存器为非零值,以便与test一起使用。ZF=1破坏性最小:cmp eax,eax-但是有一个伪依赖关系(我假设),需要一个后端uop:不是一个清零的习惯用法。RSP通常不会改变太多,所以cmp esp, esp可能是一个很好的选择。(除非它强制堆栈同步微操作)。效率最高:**xor-zeroing(如使用任何空闲寄存器的xor eax,eax)无疑是SnB系列上最有效的方法(与2字节nop**成本相同,如果需要雷克斯,则为3字节,因为您希望将r8d..r15d中的一个清零):1个前端微操作,0个后端微操作(在SnB系列上),并且FLAGS结果在其发出的同一周期内就绪。(仅在前端暂停的情况下相关,或者在同一周期内发出依赖于它的微操作,并且RS中没有任何具有就绪输入的较旧微操作,否则此类微操作对于任何执行端口都具有优先级。)
test
cmp eax,eax
cmp esp, esp
nop
标记结果:ZF=1 PF=1 SF=0 CF=0 OF=0(AF=undefined).(或者使用sub eax,eax得到定义明确的AF=0。实际上,现代CPU也会选择AF=0进行异或归零,因此它们可以用相同的方式对两个归零惯用法进行解码。Silvermont只将32位操作数大小的异或识别为归零惯用法,而不是sub。)
sub eax,eax
当然,异或零在所有其它uarch上也是非常便宜的:没有输入依赖性,并且不需要任何预先存在的寄存器值。(因此不会导致P6系列寄存器读取暂停)。因此,在最坏的情况下,它将与您可以在任何其他uarch上执行的任何其他操作绑定在一起(在那里它确实需要执行单元)。(On早期P6系列,Pentium M之前,xor-归零 * 不 * 中断依赖性;它只触发特殊的al=eax状态,以避免部分寄存器填充。但是这些CPU都不是x86-64,都是32位的。)无论如何,需要一个置零寄存器是很常见的,例如,作为0 - x的sub目的地,以便进行复制和求反,因此,通过将异或置零放在需要它的地方来利用它,也可以创建一个有用的FLAG条件。有趣但可能没有用:test al, 0的长度为2个字节,但cmp esp,esp的长度也是2个字节。
xor
0 - x
sub
test al, 0
cmp esp,esp
正如@prl所建议的,cmp same,same与任何寄存器都可以工作,而不会干扰值。我怀疑这 * 不是 * 特殊情况,因为依赖关系破坏了sub same,same在某些CPU上的方式,因此选择一个“冷”寄存器。同样是2或3个字节,1个微操作。它可以与JCC微融合,但这是愚蠢的(除非JCC也是来自其他条件的分支目标?)
cmp same,same
sub same,same
标记结果:与异或归零相同。缺点:
为了好玩,其他廉价的替代方法包括test al, 0。2个字节用于AL,3或4个字节用于任何其他8位寄存器。(雷克斯)+ opcode + modrm + imm 8。原始寄存器值无关紧要,因为imm8为零保证了reg & 0 = 0。如果你碰巧在一个寄存器中有一个1或-1,你可以销毁它,32位模式inc或dec只需要1个字节就可以设置ZF。但是在x86-64中,这至少需要2个字节。对于64位模式中的1个字节的指令,没有什么是真正有效的,并且可以设置FLAGS。
imm8
reg & 0 = 0
1
inc
sbb same,same可以设置ZF=!CF(保持CF不变),并将reg设置为0(CF=0)或-1(CF=1)。在AMD上,自Bulldozer(BD系列和Zen-family)以来,这不依赖于GP寄存器,仅依赖于CF。但在其他uarch上,这不是特殊情况,对reg存在错误依赖。在Broadwell之前的英特尔上,这是2个uops。
sbb same,same
要设置ZF=!integer_reg,很明显,普通的test reg,reg是最好的选择(比and reg,reg或or reg,reg好,除非你有意重写寄存器以避免P6寄存器读取暂停)。
and reg,reg
or reg,reg
如果寄存器值为零,则ZF=1,因此它类似于C的逻辑反运算符。
可能是setz al/test al, al。没有单一指令:我不认为任何读ZF和写FLAGS。setz在寄存器中实现ZF,那么test就是ZF = !reg。
setz al
test al, al
setz
ZF = !reg
pushf
pop rax
popf
lahf
sahf
CF具有clc/stc/cmc指令。(clc与SnB系列上的异或归零一样高效。)
clc
stc
cmc
6ojccjat2#
操作Fags低8位中任何一位的最简单方法是使用经典的LAHF/SAHF指令,该指令将Fags带入AH或从AH取出Fags,可对其应用任何位操作。
LAHF
SAHF
LAHF ; Load lower 8 bit from Flags into AH AND AH, 10111111b ; Clear bit for ZF SAHF ; Store AH back to Flags
LAHF ; Load AH from FLAGS OR AH, 01000000b ; Set bit for ZF SAHF ; Store AH back to Flags
当然,任何CMP (E)AX,(E)AX都可以更快地设置ZF,并且使用更少的代码;这样做目的是不修改其他FLAGS,如 * How to read and write x86 flags registers directly? * 和 * how to change flags manually (in assembly code) for 8086? *
CMP (E)AX,(E)AX
一些非常早期的x86-64 CPU,最值得注意的是所有
*AMD速龙64、皓龙和Turion 64修订版D之前(2005年3月)和奔腾4步进G1之前的英特尔(2005年12月)
由于该指令最初被从AMD 64指令子集中删除,但后来又被重新引入。幸运的是,这发生在x86-64成为普遍景象之前,因此只有少数早期的高端CPU受到影响,甚至更少生存下来。更重要的是,这些CPU无法运行Win10或任何64位Windows之前的Win10(参见SuperUser.SE上的this answer)。如果您真的认为有人可能会尝试在使用了17年以上的高端CPU上运行该软件,则可以通过执行CPUID(EAX= 80000001 h)并测试2^0=1来检查。
CPUID
lh80um4z3#
假设您不需要保留其他标志的值,
cmp eax, eax
3条答案
按热度按时间wmvff8tz1#
ZF=0
这是比较困难的。
cmp
在任何两个已知不相等的寄存器之间。或者cmp reg,imm
与一些寄存器不可能具有的任何值。例如,cmp reg,1
与任何已知为零的寄存器。一般来说,
test reg,reg
适用于任何已知的非0寄存器值,例如指针。**
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
:ZF=0 PF=1 SF=1 CF=0 OF=0(AF=未定义)。**如果需要在循环中执行此操作,显然可以在循环 * 外部 * 对其进行设置,前提是可以指定一个寄存器为非零值,以便与
test
一起使用。ZF=1
破坏性最小:
cmp eax,eax
-但是有一个伪依赖关系(我假设),需要一个后端uop:不是一个清零的习惯用法。RSP通常不会改变太多,所以cmp esp, esp
可能是一个很好的选择。(除非它强制堆栈同步微操作)。效率最高:**xor-zeroing(如使用任何空闲寄存器的
xor eax,eax
)无疑是SnB系列上最有效的方法(与2字节nop
**成本相同,如果需要雷克斯,则为3字节,因为您希望将r8d..r15d中的一个清零):1个前端微操作,0个后端微操作(在SnB系列上),并且FLAGS结果在其发出的同一周期内就绪。(仅在前端暂停的情况下相关,或者在同一周期内发出依赖于它的微操作,并且RS中没有任何具有就绪输入的较旧微操作,否则此类微操作对于任何执行端口都具有优先级。)标记结果:ZF=1 PF=1 SF=0 CF=0 OF=0(AF=undefined).(或者使用
sub eax,eax
得到定义明确的AF=0。实际上,现代CPU也会选择AF=0进行异或归零,因此它们可以用相同的方式对两个归零惯用法进行解码。Silvermont只将32位操作数大小的异或识别为归零惯用法,而不是sub。)当然,异或零在所有其它uarch上也是非常便宜的:没有输入依赖性,并且不需要任何预先存在的寄存器值。(因此不会导致P6系列寄存器读取暂停)。因此,在最坏的情况下,它将与您可以在任何其他uarch上执行的任何其他操作绑定在一起(在那里它确实需要执行单元)。
(On早期P6系列,Pentium M之前,
xor
-归零 * 不 * 中断依赖性;它只触发特殊的al=eax状态,以避免部分寄存器填充。但是这些CPU都不是x86-64,都是32位的。)无论如何,需要一个置零寄存器是很常见的,例如,作为
0 - x
的sub
目的地,以便进行复制和求反,因此,通过将异或置零放在需要它的地方来利用它,也可以创建一个有用的FLAG条件。有趣但可能没有用:
test al, 0
的长度为2个字节,但cmp esp,esp
的长度也是2个字节。正如@prl所建议的,
cmp same,same
与任何寄存器都可以工作,而不会干扰值。我怀疑这 * 不是 * 特殊情况,因为依赖关系破坏了sub same,same
在某些CPU上的方式,因此选择一个“冷”寄存器。同样是2或3个字节,1个微操作。它可以与JCC微融合,但这是愚蠢的(除非JCC也是来自其他条件的分支目标?)标记结果:与异或归零相同。
缺点:
为了好玩,其他廉价的替代方法包括
test al, 0
。2个字节用于AL,3或4个字节用于任何其他8位寄存器。(雷克斯)+ opcode + modrm + imm 8。原始寄存器值无关紧要,因为imm8
为零保证了reg & 0 = 0
。如果你碰巧在一个寄存器中有一个
1
或-1
,你可以销毁它,32位模式inc
或dec
只需要1个字节就可以设置ZF。但是在x86-64中,这至少需要2个字节。对于64位模式中的1个字节的指令,没有什么是真正有效的,并且可以设置FLAGS。ZF=!CF
sbb same,same
可以设置ZF=!CF(保持CF不变),并将reg设置为0(CF=0)或-1(CF=1)。在AMD上,自Bulldozer(BD系列和Zen-family)以来,这不依赖于GP寄存器,仅依赖于CF。但在其他uarch上,这不是特殊情况,对reg存在错误依赖。在Broadwell之前的英特尔上,这是2个uops。ZF=!bool(整数寄存器)
要设置ZF=!integer_reg,很明显,普通的
test reg,reg
是最好的选择(比and reg,reg
或or reg,reg
好,除非你有意重写寄存器以避免P6寄存器读取暂停)。如果寄存器值为零,则ZF=1,因此它类似于C的逻辑反运算符。
ZF=!ZF
可能是
setz al
/test al, al
。没有单一指令:我不认为任何读ZF和写FLAGS。setz
在寄存器中实现ZF,那么test
就是ZF = !reg
。其他FLAGS条件:
test
或cmp
寄存器值,则无法实现)。pushf
/pop rax
并不可怕,但是用popf
写标志非常慢(例如SKL上的1/20 c吞吐量)。它是微编码的,因为像IF这样的标志也存在于EFLAGS中,并且没有一个只包含条件码的版本或一个用于用户空间的特殊快速路径。(或者20 c * 可能是 * 快速路径。)lahf
(标志-〉AH)/sahf
(AH-〉标志)可能有用,但缺少OF。CF具有
clc
/stc
/cmc
指令。(clc
与SnB系列上的异或归零一样高效。)6ojccjat2#
操作Fags低8位中任何一位的最简单方法是使用经典的
LAHF
/SAHF
指令,该指令将Fags带入AH或从AH取出Fags,可对其应用任何位操作。关闭ZF
开启ZF
当然,任何
CMP (E)AX,(E)AX
都可以更快地设置ZF,并且使用更少的代码;这样做目的是不修改其他FLAGS,如 * How to read and write x86 flags registers directly? * 和 * how to change flags manually (in assembly code) for 8086? *CAVEAT对于早期的AMD 64- LAHF在长模式下是一个扩展
一些非常早期的x86-64 CPU,最值得注意的是所有
*AMD速龙64、皓龙和Turion 64修订版D之前(2005年3月)和
奔腾4步进G1之前的英特尔(2005年12月)
由于该指令最初被从AMD 64指令子集中删除,但后来又被重新引入。幸运的是,这发生在x86-64成为普遍景象之前,因此只有少数早期的高端CPU受到影响,甚至更少生存下来。更重要的是,这些CPU无法运行Win10或任何64位Windows之前的Win10(参见SuperUser.SE上的this answer)。
如果您真的认为有人可能会尝试在使用了17年以上的高端CPU上运行该软件,则可以通过执行
CPUID
(EAX= 80000001 h)并测试2^0=1来检查。lh80um4z3#
假设您不需要保留其他标志的值,