assembly 有效地将CPU寄存器中的所有位设置为1

czfnxgou  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(155)

为了清除所有位,你经常会看到XOR eax, eax中的异或。是否也有相反的技巧?
我所能想到的就是用一个额外的指令来反转零。

nwlls2ji

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,与大多数其他类型不同。

缺点:

  • 使用存储和加载执行单元,而不是ALU。(在只有两个整数执行管道的AMD推土机系列上,在极少数情况下可能具有吞吐量优势,但解码/发出/撤回吞吐量高于此值。但未经测试,请勿尝试。)
  • 例如,存储/重新加载延迟意味着rax在Skylake上执行后约5个周期内无法就绪。
  • (Intel):将堆栈引擎置于rsp修改模式,因此下次直接读取rsp时,它将执行堆栈同步微操作。(例如,对于add rsp, 28mov 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, k0VPBROADCASTD zmm0, eax。它们都有only 1c throughput,但它们应该打破对zmm 0旧值的依赖(与vpternlogd不同)。它们需要一个掩码或整数寄存器,您可以在循环外用kxnorw k1,k0,k0mov 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 vpcmpeqbkxnorq
在Skylake-AVX 512上,对掩码寄存器进行操作的k指令只在单个端口上运行,即使是像kandw这样的简单指令也是如此。(另请注意,当管道中存在任何512 b操作时,Skylake-AVX 512不会在端口1上运行向量微操作,因此执行单元吞吐量可能是一个真实的的瓶颈。)
没有kmov k0, imm,只有从整数或内存移动。可能没有k指令,其中same,same被检测为特殊,因此硬件在发出/重命名阶段不查找k寄存器。

bq9c1y66

bq9c1y662#

彼得已经给出了完美的答案。我只是想提一下,这也取决于上下文。
我曾经做过一个数字的sar r64, 63,我知道在某种情况下是负数,如果不是,我不需要所有的位设置值。sar的优点是它设置了一些有趣的标志,虽然解码63,真的吗?,然后我也可以做一个mov r64, -1。我猜是标志,让我这样做。
所以底线是:context。如你所知,你通常钻研汇编语言,因为你想处理你的额外知识,而不是编译器所拥有的。也许你的一些寄存器的值你不再需要存储1(所以逻辑上是true),然后就是neg。也许在程序的前面,你做了一个loop,然后(如果它是可管理的),您可以安排寄存器的使用,这样就只缺少一个not rcx

相关问题