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

Pas*_*loe 17 assembly arm x86-64 mips

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

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

Pet*_*des 17

对于大多数具有固定宽度指令的体系结构,答案可能是mov符号扩展或反转立即或mov /高对的无聊指令.例如在ARM上,mvn r0, #0(move-not).请参阅Godbolt编译器资源管理器中的 x86,ARM,ARM64和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直接快速,因为它很小.由于这个成语被广泛使用(并且归零比全部更常见),CPU设计者给予了特殊支持,现在xor eax,eax比mov eax,0Intel Sandybridge系列和其他一些CPU 更快,即使不考虑直接和间接代码大小效果.请参阅在x86汇编中将寄存器设置为零的最佳方法是什么:xor,mov或?因为我已经能够挖掘出许多微观建筑的好处.

如果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编码.(不是版本的REX.W = 1版本eax.那将是10字节mov r64, imm64).所有CPU都具有出色的性能.

通常以牺牲性能为代价来节省一些代码大小的奇怪选项:

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

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

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

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

    缺点:

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

矢量regs是不同的

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

AVX/AVX2版本也是最佳选择. 将__m256值设置为所有ONE位的最快方法


AVX512比较仅适用于屏蔽寄存器(如ymm)作为目标,因此编译器目前正在使用vpcmpeqd ymm0, ymm0, ymm0512b all-one成语.(0xff使3输入真值表的每个元素成为a vmovdqa).这不是特殊的,因为KNL或SKL上的依赖性破坏,但它在Skylake-AVX512上具有每时钟2个吞吐量.这比使用更窄的依赖性破坏AVX全能并广播或改组它更好.

如果需要在循环内重新生成all-one,显然最有效的方法是使用a vpcmpeqd来复制all-one寄存器.这甚至不在现代CPU上使用执行单元(但仍然需要前端问题带宽).但是,如果你没有向量寄存器,加载常量或是vinsertf128很好的选择.

对于AVX512,值得尝试vxorps或者也许vcmptrueps.每个只有1c吞吐量,但它们应该打破对zmm0旧值的依赖(不像vpcmpeqd).它们需要一个掩码或整数寄存器,您可以使用vxorps或在循环外部初始化它们vcmptrueps.


对于AVX512掩码寄存器,vbroadcastss可以工作,但它不依赖于当前CPU的依赖性. 英特尔的优化手册建议在收集指令之前使用它来生成全1,但建议避免使用与输出相同的输入寄存器.这避免了在循环中依赖于前一个的独立集合.由于k0经常使用,通常是一个很好的选择.

我认为vpternlogd zmm0,zmm0,zmm0, 0xff会起作用,但它可能不是特殊的,因为k0 = 1成语而不依赖于zmm0.(要设置所有64位而不是低16位,请使用AVX512BW 1)

在Skylake-AVX512上,vmov*对掩码寄存器进行操作的指令只能在单个端口上运行,即使是简单的端口也是如此[v]pcmpeq[b/w/d].(另请注意,当管道中有任何512b操作时,Skylake-AVX512将不会在port1上运行向量uop,因此执行单元吞吐量可能是一个真正的瓶颈.)

没有VPMOVM2D zmm0, k0,只从整数或内存移动.可能没有VPBROADCASTD zmm0, eax相同的指令,同样被检测为特殊指令,因此发出/重命名阶段的硬件不会为vpternlogd寄存器寻找它.

  • 半年后,我再次享受这本书。`xor ecx,ecx / lea eax` 想法适用于许多情况。 (2认同)
  • 我只是将一堆代码从`add(x,1)`更改为`sub(x,-1)`。最终的过早优化。 (2认同)
  • @BeeOnRope:当我编写它时,我并不是真的打算将其作为涵盖所有情况的参考答案。我确实链接到了 AVX/AVX2 答案,其中提到编译器在没有 AVX2 情况下对 AVX1 做了什么。是的,gcc 在使用广播负载来缩小常量方面总体上很糟糕,我认为它从来没有这样做过。(如果一个函数可以将常量提升到寄存器,而另一个函数将其用作内存源,那么它可能没有一种机制来避免重复。所以他们优先考虑保持常量简单?或者只是没有人编写常量收缩优化器传递。) (2认同)