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

Set all bits in CPU register to 1 efficiently

要清除所有位,您经常会看到 XOR eax, eax 中的异或。对面也有这样的技巧吗?

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

对于大多数具有固定宽度指令的体系结构,答案可能是无聊的单指令 mov 符号扩展或反转立即数,或一对 mov lo/high。例如在 ARM 上,mvn r0, #0(不移动)。请参阅 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 异或归零是特殊的,因为它的可变长度指令集。 历史上8086xor ax,ax直接就快,因为小。由于习惯用法被广泛使用(归零比全一更常见),CPU 设计师给予了它特殊的支持,现在 xor eax,eax 在英特尔 Sandybridge 系列上比 mov eax,0 更快和其他一些 CPUs,即使不考虑直接和间接代码大小的影响。请参阅 ,了解我所能挖掘的尽可能多的微架构优势。

如果 x86 有一个固定宽度的指令集,我想知道 mov reg, 0 是否会得到像异或归零一样多的特殊待遇?也许,因为在编写 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 个字节(32 位 eax 为 4 个字节)。缺点:前端有两个微指令。在最近的 Intel 上,scheduler/execution 单元仍然只有一个未融合域 uop,其中 在前端处理。 mov-立即总是需要一个执行单元。 (但整数 ALU 吞吐量很少成为可以使用任何端口的指令的瓶颈;额外的前端压力是问题所在)
  • xor ecx,ecx / lea eax, [rcx-1] 2 个常量总共 5 个字节(rax 为 6 个字节):留下一个单独的归零寄存器。如果您已经想要一个清零的寄存器,那么这几乎没有任何缺点。 lea 可以 运行 在大多数 CPU 上比 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 个字节。慢但小。仅推荐用于漏洞利用/代码高尔夫。 适用于任何符号扩展的 imm8,与其他大多数不同。

    缺点:

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

矢量 regs 不同

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

对于AVX2ymm等价的vpcmpeqd ymm0, ymm0, ymm0也是最好的选择。

对于没有 AVX2 的 AVX,选择不太明确:没有明显的最佳方法。编译器使用 various strategies:gcc 更喜欢用 vmovdqa 加载一个 32 字节常量,而旧的 clang 使用 128 位 vpcmpeqd 后跟一个交叉通道 vinsertf128 来填充高一半。较新的 clang 使用 vxorps 将寄存器归零,然后 vcmptrueps 将其填充。这在道德上等同于 vpcmpeqd 方法,但需要 vxorps 来打破对寄存器先前版本的依赖,并且 vcmptrueps 的延迟为 3。它使一个合理的默认选择。

从 32 位值执行 vbroadcastss 可能严格来说优于加载方法,但很难让编译器生成它。

最好的方法可能取决于周围的代码。


AVX512 比较仅适用于掩码寄存器(如 k0)作为目标,因此编译器当前使用 vpternlogd zmm0,zmm0,zmm0, 0xff 作为 512b 的全一习语。 (0xff 使 3-input truth-table 的每个元素成为 1)。这不是 KNL 或 SKL 上的依赖性破坏的特殊情况,但它在 Skylake-AVX512 上具有每时钟 2 个吞吐量。这比使用更窄的依赖性破坏 AVX all-ones 和广播或洗牌要好。

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

对于 AVX512,值得尝试 VPMOVM2D zmm0, k0 或者 VPBROADCASTD zmm0, eax。每个都有 only 1c throughput,但它们应该打破对 zmm0 旧值的依赖(不像 vpternlogd)。它们需要一个掩码或整数寄存器,您可以使用 kxnorw k1,k0,k0mov eax, -1.

在循环外对其进行初始化

对于 AVX512 掩码寄存器kxnorw k1,k0,k0 有效,但它不会破坏当前的 CPUs。 Intel's optimization manual 建议在收集指令之前使用它来生成全 1,但建议避免使用与输出相同的输入寄存器。这避免了使一个在其他方面独立的收集依赖于循环中的前一个收集。由于 k0 通常不被使用,因此通常是一个不错的阅读选择。

我认为 vpcmpeqd k1, zmm0,zmm0 会起作用,但它可能不是特殊情况下的 k0=1 习语,不依赖于 zmm0。 (要设置所有 64 位而不只是低 16 位,请使用 AVX512BW vpcmpeqb

在 Skylake-AVX512 上,k 操作屏蔽寄存器 , even simple ones like kandw 的指令。 (另请注意,当管道中有任何 512b 操作时,Skylake-AVX512 不会 运行 端口 1 上的矢量微指令,因此执行单元吞吐量可能是一个真正的瓶颈。)

没有kmov k0, imm,只能从整数或内存中移动。可能没有 k 指令将 same,same 检测为特殊指令,因此 issue/rename 阶段的硬件不会为 k 寄存器寻找它。

Peter 已经给出了完美的答案。我只是想提一下,这也取决于上下文。

我曾经做过 sar r64, 63 一个数字,我知道在某些情况下会是负数,如果不是,我不需要所有位设置值。 sar 的优点是它设置了一些有趣的标志,尽管解码 63,真的吗?那么我也可以做 mov r64, -1。我猜是旗帜让我无论如何都可以这样做。

所以底线:上下文。如您所知,您通常会钻研汇编语言,因为您想要处理您拥有的额外知识,但编译器没有。也许您不再需要其值的某些寄存器存储了 1(如此合乎逻辑 true),然后只是 neg。也许在你的程序的前面某个地方你做了一个 loop,然后(假设它是可管理的)你可以安排你的寄存器使用,所以一个 not rcx 就是所缺少的。