为了清除所有位,你经常会看到XOR eax, eax中的异或。是否也有相反的技巧?我所能想到的就是用一个额外的指令来反转零。
XOR eax, eax
nwlls2ji1#
对于大多数使用固定宽度指令的架构,答案可能是一条无聊的指令mov(符号扩展或反转立即数),或者是一个mov lo/high对。例如,在ARM上,mvn r0, #0(move-not)。请参阅Godbolt编译器资源管理器上x86、ARM、ARM 64和MIPS的gcc asm输出。IDK关于zseries asm或机器码的任何内容。在ARM中,eor r0,r0,r0明显比mov-immediate差。它依赖于旧值,没有特殊情况处理。内存依赖排序规则阻止ARM uarch将其特殊化,即使它们想这样做。大多数其他RISC ISA也是如此,它们具有弱序内存,但不需要memory_order_consume的屏障(C++11术语)。x86 xor-zeroing由于其可变长度指令集而具有特殊性。从历史上看,8086 xor ax,ax之所以速度快,直接是因为它很小。自从这个习惯用法被广泛使用以来(而且清零比全1更常见),CPU设计人员给予了它特殊的支持,现在xor eax,eax在Intel Sandybridge系列和一些其他CPU上比mov eax,0更快,即使不考虑直接和间接的代码大小影响。请参阅在x86汇编中将寄存器设置为零的最佳方法:xor、mov或and?我能挖掘到的微体系结构的优点。如果x86有一个固定宽度的指令集,我想知道mov reg, 0是否会得到像xor-zeroing那样的特殊处理?也许吧,因为在写low 8或low 16之前中断依赖关系是很重要的。最佳性能的标准选项:
mov
mvn r0, #0
eor r0,r0,r0
memory_order_consume
xor ax,ax
xor eax,eax
mov eax,0
mov reg, 0
*mov eax, -1:5字节,使用mov r32, imm32编码。(不幸的是,没有符号扩展mov r32, imm8)。在所有CPU上都有出色的性能。6字节用于r8 d-r15 d(雷克斯前缀)。*mov rax, -1:7字节,使用mov r/m64, sign-extended-imm32编码。(不是eax版本的雷克斯.W=1版本。那将是10字节mov r64, imm64)。在所有CPU上都有出色的性能。
mov eax, -1
mov r32, imm32
mov r32, imm8
mov rax, -1
mov r/m64, sign-extended-imm32
eax
mov r64, imm64
保存一些代码大小的奇怪选项通常以牺牲性能为代价:(See也称为Tips for golfing in x86/x64 machine code)
*xor eax,eax/dec rax(或not rax):5个字节(32位eax为4个字节,32位 * 模式 * 中为3个字节,其中存在1个字节dec eax。64位模式使用这些1字节指令作为雷克斯前缀)。缺点:两个微操作用于前端。在最近的Intel上,xor-zeroing在前端处理,仍然只有一个非融合域微操作用于调度器/执行单元。mov-立即数总是需要一个执行单元。(但是整数ALU吞吐量很少是可以使用任何端口的指令的瓶颈;额外的前端压力是问题所在)***xor ecx,ecx/lea eax, [rcx-1]**2个常量共5个字节(rax为6个字节):保留一个单独的置零寄存器。如果你已经想要一个置零寄存器,这几乎没有什么坏处。在大多数CPU上,lea可以在比mov r,i更少的端口上运行,但是由于这是一个新依赖链的开始,CPU可以在它发出后的任何空闲执行端口周期中运行它。
dec rax
not rax
dec eax
xor ecx,ecx
lea eax, [rcx-1]
rax
lea
mov r,i
同样的技巧也适用于任何两个相邻的常量,如果你用mov reg, imm32(或push imm 8/pop)做第一个,用lea r32, [base + disp8]做第二个。disp 8的范围是-128到+127,否则你需要一个disp32。在一个循环之后,你可能有一个已知为零的寄存器,但是相对于它的莱亚会产生一个假的依赖关系,而mov-immediate则不会。分支预测+推测性执行可以打破控制依赖关系,尽管循环分支经常会错误预测它们的最后一次迭代,除非行程计数很低。
mov reg, imm32
lea r32, [base + disp8]
disp32
*or eax, -1:3个字节(rax为4个字节),使用or r/m32, sign-extended-imm8编码。缺点:false依赖于寄存器的旧值。*push -1 / pop rax:3个字节。速度慢但很小。建议仅用于漏洞利用/代码高尔夫。适用于任何sign-extended-imm 8,与大多数其他类型不同。
or eax, -1
or r/m32, sign-extended-imm8
push -1
pop rax
缺点:
rsp
add rsp, 28
mov eax, [rsp+8]
使用**pcmpeqd xmm0,xmm0将向量寄存器设置为全1在大多数CPU上是特殊情况,因为依赖关系中断(不是Silvermont/KNL),但仍然需要一个执行单元来实际写入1。pcmpeqb/w/d/q都可以工作,但q在某些CPU上速度较慢,机器码较长。对于AVX 2**,与ymm等效的vpcmpeqd ymm0, ymm0, ymm0也是最佳选择。(或b/w是等效的,但vpcmpeqq的机器代码更长。)
pcmpeqd xmm0,xmm0
pcmpeqb/w/d/q
q
ymm
vpcmpeqd ymm0, ymm0, ymm0
vpcmpeqq
对于AVX而不带AVX 2,选择不太明确:没有一种明显的最佳方法。编译器使用various strategies:gcc倾向于用vmovdqa加载一个32字节的常量,而旧的clang使用128位的vpcmpeqd,后跟一个交叉通道vinsertf128来填充高半部分。新的clang使用vxorps来将寄存器清零,然后使用vcmptrueps来填充1。这在道德上等同于vpcmpeqd方法,但是需要vxorps来打破对寄存器先前版本的依赖性,并且vcmptrueps的延迟为3。这是一个合理的默认选择。从32位值执行vbroadcastss可能严格地说比load方法更好,但是很难让编译器生成这种方法。最好的方法可能取决于周围的代码。将__m256值设置为全1位的最快方法
vmovdqa
vpcmpeqd
vinsertf128
vxorps
vcmptrueps
vbroadcastss
AVX 512比较仅适用于掩码寄存器(如k0)作为目标,因此编译器目前使用**vpternlogd zmm0,zmm0,zmm0, 0xff**作为512 b全1的习惯用法。(0xff使3输入真值表的每个元素都是1)。这不是KNL或SKL上依赖性中断的特殊情况,但它在Skylake-AVX 512上具有每时钟2个的吞吐量。这优于使用更窄的依赖性打破AVX全1并广播或洗牌。
k0
vpternlogd zmm0,zmm0,zmm0, 0xff
1
如果你需要在循环中重新生成全1,显然最有效的方法是使用vmov*来复制全1寄存器。在现代CPU上,这甚至不使用执行单元(但仍然占用前端发布带宽)。但如果你没有向量寄存器,加载常量或[v]pcmpeq[b/w/d]是很好的选择。对于AVX 512,值得尝试VPMOVM2D zmm0, k0或VPBROADCASTD zmm0, eax。它们都有only 1c throughput,但它们应该打破对zmm 0旧值的依赖(与vpternlogd不同)。它们需要一个掩码或整数寄存器,您可以在循环外用kxnorw k1,k0,k0或mov eax, -1初始化。对于AVX 512掩码寄存器,kxnorw k1,k0,k0可以工作,但在当前CPU上不能中断依赖关系。Intel's optimization manual建议在gather指令之前使用它生成全1,但建议避免使用相同的输入寄存器作为输出。这可以避免在循环中使独立的gather依赖于前一个gather。由于k0通常不使用,它通常是一个很好的阅读选择。我认为vpcmpeqd k1, zmm0,zmm0可以工作,但它可能不是特殊情况,因为k1=1的习惯用法不依赖于zmm 0。(要设置所有64位而不是低16位,请使用AVX 512 BW vpcmpeqb或kxnorq)在Skylake-AVX 512上,对掩码寄存器进行操作的k指令只在单个端口上运行,即使是像kandw这样的简单指令也是如此。(另请注意,当管道中存在任何512 b操作时,Skylake-AVX 512不会在端口1上运行向量微操作,因此执行单元吞吐量可能是一个真实的的瓶颈。)没有kmov k0, imm,只有从整数或内存移动。可能没有k指令,其中same,same被检测为特殊,因此硬件在发出/重命名阶段不查找k寄存器。
vmov*
[v]pcmpeq[b/w/d]
VPMOVM2D zmm0, k0
VPBROADCASTD zmm0, eax
vpternlogd
kxnorw k1,k0,k0
vpcmpeqd k1, zmm0,zmm0
vpcmpeqb
kxnorq
k
kandw
kmov k0, imm
bq9c1y662#
彼得已经给出了完美的答案。我只是想提一下,这也取决于上下文。我曾经做过一个数字的sar r64, 63,我知道在某种情况下是负数,如果不是,我不需要所有的位设置值。sar的优点是它设置了一些有趣的标志,虽然解码63,真的吗?,然后我也可以做一个mov r64, -1。我猜是标志,让我这样做。所以底线是:context。如你所知,你通常钻研汇编语言,因为你想处理你的额外知识,而不是编译器所拥有的。也许你的一些寄存器的值你不再需要存储1(所以逻辑上是true),然后就是neg。也许在程序的前面,你做了一个loop,然后(如果它是可管理的),您可以安排寄存器的使用,这样就只缺少一个not rcx。
sar r64, 63
sar
63
mov r64, -1
true
neg
loop
not rcx
2条答案
按热度按时间nwlls2ji1#
对于大多数使用固定宽度指令的架构,答案可能是一条无聊的指令
mov
(符号扩展或反转立即数),或者是一个mov lo/high对。例如,在ARM上,mvn r0, #0
(move-not)。请参阅Godbolt编译器资源管理器上x86、ARM、ARM 64和MIPS的gcc asm输出。IDK关于zseries asm或机器码的任何内容。在ARM中,
eor r0,r0,r0
明显比mov-immediate差。它依赖于旧值,没有特殊情况处理。内存依赖排序规则阻止ARM uarch将其特殊化,即使它们想这样做。大多数其他RISC ISA也是如此,它们具有弱序内存,但不需要memory_order_consume
的屏障(C++11术语)。x86 xor-zeroing由于其可变长度指令集而具有特殊性。从历史上看,8086
xor ax,ax
之所以速度快,直接是因为它很小。自从这个习惯用法被广泛使用以来(而且清零比全1更常见),CPU设计人员给予了它特殊的支持,现在xor eax,eax
在Intel Sandybridge系列和一些其他CPU上比mov eax,0
更快,即使不考虑直接和间接的代码大小影响。请参阅在x86汇编中将寄存器设置为零的最佳方法:xor、mov或and?我能挖掘到的微体系结构的优点。如果x86有一个固定宽度的指令集,我想知道
mov reg, 0
是否会得到像xor-zeroing那样的特殊处理?也许吧,因为在写low 8或low 16之前中断依赖关系是很重要的。最佳性能的标准选项:
*
mov eax, -1
:5字节,使用mov r32, imm32
编码。(不幸的是,没有符号扩展mov r32, imm8
)。在所有CPU上都有出色的性能。6字节用于r8 d-r15 d(雷克斯前缀)。*
mov rax, -1
:7字节,使用mov r/m64, sign-extended-imm32
编码。(不是eax
版本的雷克斯.W=1版本。那将是10字节mov r64, imm64
)。在所有CPU上都有出色的性能。保存一些代码大小的奇怪选项通常以牺牲性能为代价:
(See也称为Tips for golfing in x86/x64 machine code)
*
xor eax,eax
/dec rax
(或not rax
):5个字节(32位eax
为4个字节,32位 * 模式 * 中为3个字节,其中存在1个字节dec eax
。64位模式使用这些1字节指令作为雷克斯前缀)。缺点:两个微操作用于前端。在最近的Intel上,xor-zeroing在前端处理,仍然只有一个非融合域微操作用于调度器/执行单元。mov
-立即数总是需要一个执行单元。(但是整数ALU吞吐量很少是可以使用任何端口的指令的瓶颈;额外的前端压力是问题所在)***
xor ecx,ecx
/lea eax, [rcx-1]
**2个常量共5个字节(rax
为6个字节):保留一个单独的置零寄存器。如果你已经想要一个置零寄存器,这几乎没有什么坏处。在大多数CPU上,lea
可以在比mov r,i
更少的端口上运行,但是由于这是一个新依赖链的开始,CPU可以在它发出后的任何空闲执行端口周期中运行它。同样的技巧也适用于任何两个相邻的常量,如果你用
mov reg, imm32
(或push imm 8/pop)做第一个,用lea r32, [base + disp8]
做第二个。disp 8的范围是-128到+127,否则你需要一个disp32
。在一个循环之后,你可能有一个已知为零的寄存器,但是相对于它的莱亚会产生一个假的依赖关系,而mov-immediate则不会。分支预测+推测性执行可以打破控制依赖关系,尽管循环分支经常会错误预测它们的最后一次迭代,除非行程计数很低。
*
or eax, -1
:3个字节(rax
为4个字节),使用or r/m32, sign-extended-imm8
编码。缺点:false依赖于寄存器的旧值。*
push -1
/pop rax
:3个字节。速度慢但很小。建议仅用于漏洞利用/代码高尔夫。适用于任何sign-extended-imm 8,与大多数其他类型不同。缺点:
rax
在Skylake上执行后约5个周期内无法就绪。rsp
时,它将执行堆栈同步微操作。(例如,对于add rsp, 28
或mov eax, [rsp+8]
)。向量寄存器不同
使用**
pcmpeqd xmm0,xmm0
将向量寄存器设置为全1在大多数CPU上是特殊情况,因为依赖关系中断(不是Silvermont/KNL),但仍然需要一个执行单元来实际写入1。pcmpeqb/w/d/q
都可以工作,但q
在某些CPU上速度较慢,机器码较长。对于AVX 2**,与
ymm
等效的vpcmpeqd ymm0, ymm0, ymm0
也是最佳选择。(或b/w是等效的,但vpcmpeqq
的机器代码更长。)对于AVX而不带AVX 2,选择不太明确:没有一种明显的最佳方法。编译器使用various strategies:gcc倾向于用
vmovdqa
加载一个32字节的常量,而旧的clang使用128位的vpcmpeqd
,后跟一个交叉通道vinsertf128
来填充高半部分。新的clang使用vxorps
来将寄存器清零,然后使用vcmptrueps
来填充1。这在道德上等同于vpcmpeqd
方法,但是需要vxorps
来打破对寄存器先前版本的依赖性,并且vcmptrueps
的延迟为3。这是一个合理的默认选择。从32位值执行
vbroadcastss
可能严格地说比load方法更好,但是很难让编译器生成这种方法。最好的方法可能取决于周围的代码。
将__m256值设置为全1位的最快方法
AVX 512比较仅适用于掩码寄存器(如
k0
)作为目标,因此编译器目前使用**vpternlogd zmm0,zmm0,zmm0, 0xff
**作为512 b全1的习惯用法。(0xff使3输入真值表的每个元素都是1
)。这不是KNL或SKL上依赖性中断的特殊情况,但它在Skylake-AVX 512上具有每时钟2个的吞吐量。这优于使用更窄的依赖性打破AVX全1并广播或洗牌。如果你需要在循环中重新生成全1,显然最有效的方法是使用
vmov*
来复制全1寄存器。在现代CPU上,这甚至不使用执行单元(但仍然占用前端发布带宽)。但如果你没有向量寄存器,加载常量或[v]pcmpeq[b/w/d]
是很好的选择。对于AVX 512,值得尝试
VPMOVM2D zmm0, k0
或VPBROADCASTD zmm0, eax
。它们都有only 1c throughput,但它们应该打破对zmm 0旧值的依赖(与vpternlogd
不同)。它们需要一个掩码或整数寄存器,您可以在循环外用kxnorw k1,k0,k0
或mov eax, -1
初始化。对于AVX 512掩码寄存器,
kxnorw k1,k0,k0
可以工作,但在当前CPU上不能中断依赖关系。Intel's optimization manual建议在gather指令之前使用它生成全1,但建议避免使用相同的输入寄存器作为输出。这可以避免在循环中使独立的gather依赖于前一个gather。由于k0
通常不使用,它通常是一个很好的阅读选择。我认为
vpcmpeqd k1, zmm0,zmm0
可以工作,但它可能不是特殊情况,因为k1=1的习惯用法不依赖于zmm 0。(要设置所有64位而不是低16位,请使用AVX 512 BWvpcmpeqb
或kxnorq
)在Skylake-AVX 512上,对掩码寄存器进行操作的
k
指令只在单个端口上运行,即使是像kandw
这样的简单指令也是如此。(另请注意,当管道中存在任何512 b操作时,Skylake-AVX 512不会在端口1上运行向量微操作,因此执行单元吞吐量可能是一个真实的的瓶颈。)没有
kmov k0, imm
,只有从整数或内存移动。可能没有k
指令,其中same,same被检测为特殊,因此硬件在发出/重命名阶段不查找k
寄存器。bq9c1y662#
彼得已经给出了完美的答案。我只是想提一下,这也取决于上下文。
我曾经做过一个数字的
sar r64, 63
,我知道在某种情况下是负数,如果不是,我不需要所有的位设置值。sar
的优点是它设置了一些有趣的标志,虽然解码63
,真的吗?,然后我也可以做一个mov r64, -1
。我猜是标志,让我这样做。所以底线是:context。如你所知,你通常钻研汇编语言,因为你想处理你的额外知识,而不是编译器所拥有的。也许你的一些寄存器的值你不再需要存储
1
(所以逻辑上是true
),然后就是neg
。也许在程序的前面,你做了一个loop
,然后(如果它是可管理的),您可以安排寄存器的使用,这样就只缺少一个not rcx
。