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

时间:2017-07-14 14:14:45

标签: assembly arm mips x86-64

要清除所有位,您经常会看到独占或XOR eax, eax。反过来也有这样的伎俩吗?

我能想到的只是用额外的指令反转零。

2 个答案:

答案 0 :(得分:14)

对于大多数具有固定宽度指令的体系结构,答案可能是一个无聊的指令mov的符号扩展或反转立即,或mov lo / high对。例如在ARM上,mvn r0, #0(move-not)。请参阅x86,ARM,ARM64和MIPS的gcc asm输出on the Godbolt compiler explorer。 IDK关于zseries asm或机器代码的任何内容。

在ARM中,eor r0,r0,r0明显比mov-immediate差。它取决于旧值,没有特殊情况处理。内存依赖性排序规则prevent an ARM uarch from special-casing it even if they wanted to.对于大多数其他具有弱排序内存的RISC ISA也是如此,但memory_order_consume不需要障碍(在C ++ 11术语中)。

x86 xor-zeroing因其可变长度指令集而特殊。  从历史上看,8086 xor ax,ax直接快速因为它很小。由于习惯用法被广泛使用(并且归零比所有用户更常见),CPU设计人员给予了特殊支持,现在xor eax,eax比英特尔Sandybridge系列和其他一些CPU上的mov eax,0更快即使不考虑直接和间接的代码大小效果。请参阅What is the best way to set a register to zero in x86 assembly: xor, mov or and?,了解我能够挖掘的许多微观架构优势。

如果x86有一个固定宽度的指令集,我想知道mov reg, 0是否会得到与xor-zeroing一样多的特殊处理?也许,因为写入low8或low16之前的依赖性破坏很重要。

最佳表现的标准选项:

  • mov eax, -1 :5个字节,使用mov r32, imm32编码。 (不幸的是,没有符号扩展mov r32, imm8)。所有CPU都具有出色的性能。 6个字节用于r8-r15(REX前缀)。
  • mov rax, -1 :7个字节,使用mov r/m64, sign-extended-imm32编码。 (不是eax版本的REX.W = 1版本。那将是10字节mov r64, imm64)。所有CPU都具有出色的性能。

保存一些代码大小的奇怪选项通常以牺牲性能为代价

  • xor eax,eax / dec rax (或not rax):5个字节(4个用于32位eax)。缺点:前端有两个uops。在最近的英特尔上,调度程序/执行单元仍然只有一个未融合的域uop,前端处理xor-zeroingmov - 立即需要执行单元。 (但整数ALU吞吐量很少是可以使用任何端口的指令的瓶颈;额外的前端压力是问题)
  • xor ecx,ecx / lea eax, [rcx-1] 2个常量总共5个字节(rax为6个字节):留下一个单独的归零寄存器< / strong>即可。如果您已经想要一个归零寄存器,那么这几乎没有任何缺点。在大多数CPU上,lea可以在比mov r,i更少的端口上运行,但由于这是新依赖关系链的开始,因此CPU可以在发出任何备用执行端口周期后运行它。

    同样的技巧适用于任何两个附近的常量,如果你使用mov reg, imm32执行第一个,而使用lea r32, [base + disp8]执行第二个常量。 disp8的范围是-128到+127,否则你需要一个disp32

  • or eax, -1 :3个字节(rax为4个),使用or r/m32, sign-extended-imm8编码。缺点:对寄存器旧值的错误依赖。

  • push -1 / pop rax :3个字节。慢但很小。建议仅用于漏洞/代码高尔夫。 适用于任何sign-extended-imm8 ,与大多数其他人不同。

    缺点:

    • 使用存储和加载执行单元,而不是ALU。 (在极少数情况下,AMD Bulldozer系列只有两个整数执行管道,但解码/发布/退役吞吐量高于此值,可能具有吞吐量优势。但不要在没有测试的情况下尝试它。)
    • 例如,
    • 存储/重新加载延迟意味着rax在Skylake执行后不会准备好约5个周期。
    • (英特尔):将堆栈引擎置于rsp修改模式,因此下次直接读取rsp时,将需要堆栈同步uop。 (例如add rsp, 28mov eax, [rsp+8])。
    • 商店可能会错过缓存,从而触发额外的内存流量。 (如果您没有在长循环内触及堆栈,则可能。)

Vector regs不同

将向量寄存器设置为带有 pcmpeqd xmm0,xmm0 的全向寄存器是特殊的,在大多数CPU上作为依赖性破坏(不是Silvermont / KNL),但仍需要执行单元来实际写入那些。 pcmpeqb/w/d/q一切正常,但q在某些CPU上速度较慢。

AVX / AVX2版本也是最佳选择。 Fastest way to set __m256 value to all ONE bits

AVX512 比较仅适用于屏蔽寄存器(如k0)作为目标,因此编译器目前正在使用 vpternlogd zmm0,zmm0,zmm0, 0xff 作为512b all-one成语。 (0xff使3输入真值表的每个元素成为1)。这不是特殊的,因为KNL或SKL上的依赖性破坏,但它在Skylake-AVX512上具有每时钟2个吞吐量。这比使用更窄的依赖性破坏AVX all-ones并广播或改组它更胜一筹。

如果需要在循环内重新生成all-one,显然最有效的方法是使用vmov*复制all-one寄存器。这甚至不能在现代CPU上使用执行单元(但仍然需要前端问题带宽)。但是,如果您没有使用向量寄存器,则加载常量或[v]pcmpeq[b/w/d]是不错的选择。

对于AVX512,值得尝试VPMOVM2D zmm0, k0VPBROADCASTD zmm0, eax。每个都有only 1c throughput,但它们应该破坏zmm0旧值的依赖性(与vpternlogd不同)。它们需要使用kxnorw k1,k0,k0mov eax, -1在循环外部初始化的掩码或整数寄存器。

对于 AVX512掩码寄存器kxnorw k1,k0,k0可以正常工作,但它对当前CPU没有依赖性。 Intel's optimization manual建议在收集指令之前使用它来生成全1,但建议避免使用相同的输入寄存器作为输出。这避免了在循环中依赖于前一个的独立集合。由于k0经常未被使用,因此通常是一个很好的选择。

我认为vpcmpeqd k1, zmm0,zmm0可行,但它可能不是特殊的,因为k0 = 1成语而不依赖于zmm0。 (要设置所有64位而不是低16位,请使用AVX512BW vpcmpeqb

在Skylake-AVX512上,k指令对掩码寄存器only run on a single port起作用,甚至是kandw之类的简单指令。 (另请注意,当管道中有512b操作时,Skylake-AVX512不会在port1上运行向量uop,因此执行单元吞吐量可能是一个真正的瓶颈。)

没有kmov k0, imm,只从整数或内存移动。可能没有k指令相同,同样被检测为特殊指令,因此发布/重命名阶段的硬件不会为k寄存器寻找它。

答案 1 :(得分:2)

彼得已经提供了一个完美的答案。我只想提一下,它也取决于上下文。

在某种情况下,我一次做过一个sar r64, 63的数字,我知道该数字将为负,如果不是,则不需要所有位都设置值。 sar的优点是它设置了一些有趣的标志,尽管确实对63进行了解码,但是我也可以完成mov r64, -1。我想那还是让我照做的旗帜。

因此,底线是:上下文。如您所知,您通常会深入研究汇编语言,因为您想处理自己而不是编译器所拥有的知识。也许某些不再需要其值的寄存器已经存储了1(逻辑true),然后只是存储了neg。也许您在程序的较早位置做了一个loop,然后(假设它是可管理的)可以安排寄存器的使用,因此not rcx就是所缺少的。