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

Posted

技术标签:

【中文标题】有效地将 CPU 寄存器中的所有位设置为 1【英文标题】:Set all bits in CPU register to 1 efficiently 【发布时间】:2017-12-19 16:25:44 【问题描述】:

要清除所有位,您经常会在XOR eax, eax 中看到独占或。对面也有这样的招数吗?

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

【问题讨论】:

例如or eax, -1 -1 被编码到指令中 or eax, -1 相对于mov eax, -1 的优势几乎为零,并且它可能引入了对eax 先前内容的错误依赖。 xor eax, eax 很方便,因为它具有非常紧凑的编码(实际上是寄存器重命名电路中的一种特殊情况)。 push -1; pop rax 只是 3 个字节,尽管 6A FF 58 Sbb reg, reg 已知 CF=1? 【参考方案1】:

对于大多数具有固定宽度指令的体系结构,答案可能是一个无聊的单指令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 xor-zeroing 是特殊的,因为它的可变长度指令集。 从历史上看,8086 xor ax,ax 直接很快因为它很小。由于该惯用语被广泛使用(并且归零比全部使用更常见),CPU 设计人员给予了特别支持,现在xor eax,eax 在 Intel 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 是否会得到与异或归零一样多的特殊处理?也许是因为在编写 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)。缺点:前端有两个微指令。最近英特尔上的调度程序/执行单元仍然只有一个未融合域微指令,其中xor-zeroing 在前端处理。 mov-immediate 总是需要一个执行单元。 (但对于可以使用任何端口的指令而言,整数 ALU 吞吐量很少成为瓶颈;额外的前端压力是问题所在)

xor ecx,ecx / lea eax, [rcx-1] 2 个常量共 5 个字节(rax 为 6 个字节):留下一个单独的归零寄存器。如果您已经想要一个归零的寄存器,那么这几乎没有缺点。在大多数 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 个周期内不会准备好。 (Intel):将堆栈引擎置于 rsp-modified 模式,因此下次您直接读取 rsp 时,它将采用堆栈同步 uop。 (例如add rsp, 28,或mov eax, [rsp+8])。 存储可能会在缓存中丢失,从而触发额外的内存流量。 (如果您没有在一个长循环中触及堆栈,则可能)。

向量 reg 不同

使用 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 将其填充为 1。这是vpcmpeqd 方法的道德等价物,但需要vxorps 来打破对寄存器先前版本的依赖,并且vcmptrueps 的延迟为3。它是一个合理的默认选择。

从 32 位值执行 vbroadcastss 可能比加载方法更好,但很难让编译器生成它。

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

Fastest way to set __m256 value to all ONE bits


AVX512 比较只能使用掩码寄存器(如 k0)作为目标,因此编译器当前使用 vpternlogd zmm0,zmm0,zmm0, 0xff 作为 512b 全一成语。 (0xff 使 3 输入真值表的每个元素都成为 1)。这并不是对 KNL 或 SKL 的依赖破坏的特殊情况,但它在 Skylake-AVX512 上具有每时钟 2 个吞吐量。这优于使用更窄的依赖关系破坏 AVX all-ones 并广播或改组它。

如果您需要在循环中重新生成全一,显然最有效的方法是使用vmov* 复制全一寄存器。这甚至不使用现代 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 建议在收集指令之前使用它来生成全一,但建议避免使用与输出相同的输入寄存器。这避免了使一个独立的集合依赖于循环中的前一个集合。由于k0 经常未被使用,因此通常是一个不错的读取选择。

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

在 Skylake-AVX512 上,k 指令在屏蔽寄存器 only run on a single port 上运行,甚至是像 kandw 这样的简单指令。 (另请注意,当管道中有任何 512b 操作时,Skylake-AVX512 不会在端口 1 上运行向量微指令,因此执行单元吞吐量可能是一个真正的瓶颈。)

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

【讨论】:

半年后我再次阅读这篇文章。 xor ecx,ecx / lea eax 想法适用于许多情况。 @PascaldeKloe:是的,这很有趣,因为它是为数不多的不影响性能的产品之一,如果您已经拥有任何已知的注册机,那么它就像 push imm8 / pop 一样短价值。 Very useful for code-golf, too. 我刚刚把一堆代码从add(x, 1)改成了sub(x, -1)。最终的过早优化。 @BeeOnRope:当我写它时,我并不是真的打算将其作为涵盖所有情况的参考答案。我确实链接到了一个 AVX/AVX2 答案,其中提到了编译器对没有 AVX2 情况的 AVX1 所做的事情。是的,gcc 在使用广播负载来缩小常量方面通常很糟糕,我认为它从来没有这样做过。 (如果一个函数可以将常量提升到寄存器而另一个函数将其用作内存源,那么它可能没有避免重复的机制。所以他们优先考虑保持常量简单?或者只是没有人编写常量收缩优化程序。) @BeeOnRope:请随意编辑,否则我最终可能会解决它。【参考方案2】:

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

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

所以底线:上下文。如您所知,您通常会深入研究汇编语言,因为您想处理自己的额外知识,而不是编译器拥有的知识。也许您不再需要某些值的寄存器存储了1(如此合乎逻辑的true),然后只是neg。也许您在程序的早期某个地方执行了loop,然后(如果它是可管理的)您可以安排您的寄存器使用,因此缺少not rcx

【讨论】:

你的意思是sar r64, 63?您需要一个算术而非逻辑右移来将符号位广播到所有位。 有趣,并且代码大小与or r64, -1 相同(REX + 单字节操作码 + ModRM + imm8),所以如果你想要标志结果,那么这可能是一个胜利,如果你'不会成为移位端口吞吐量的瓶颈。是的,notneg 将比or imm8 节省一个字节,同时对旧值具有相同的“假”依赖性。太糟糕了 x86-64 没有使用一些从删除 BCD 指令和push seg_reg 中释放出来的操作码来获得mov r/m32, sign-extended-imm8 操作码。这将给我们 3 字节 mov eax, -1 和 4 字节 mov rax,-1(对比 5 和 7) 是的,当然是sar,而不是shr。妥当注明。感谢您指出。我通常不太关心空间,但关心速度。 如果您正在优化现代无序 x86-64 的速度,为什么要使用 negnot 而不是 mov r64, -1?您是否发现使用较短的 insn 有助于避免前端瓶颈?如果您还需要在 FLAGS 中设置某些内容,那么可以确定,但 NOT 不会影响标志。你提到了loop,除了 AMD Bulldozer-family 和 Ryzen,它在所有东西上都很慢,所以如果优化速度,你不会使用它,除非你的代码只能在最近的 AMD 上运行。 Why is the loop instruction slow? Couldn't Intel have implemented it efficiently? 我也不喜欢我的代码可读。 mov r64, -1 太明显了。我通常为未来编写代码,为未来的处理器编写代码。使用更专业的指令会给 CPU 更多的提示,它不必解开所有的东西(尽管他们今天真的很擅长)。

以上是关于有效地将 CPU 寄存器中的所有位设置为 1的主要内容,如果未能解决你的问题,请参考以下文章

在单臂霓虹灯寄存器中有效地将 8 位数字扩展到 12 位

如何有效地将 zmm 寄存器的低 64 位保存到内存中?

标志寄存器

cpu架构-x86

将 __m256i 寄存器转换为 uint64_t 位掩码,以便每个字节值处的值是输出中的设置位

通用寄存器都有哪些?