在 x86-64 asm 中:如果源操作数是两个立即数,是否有一种方法可以优化两个相邻的 32 位存储/写入内存?

Cec*_*ard 8 optimization assembly x86-64 micro-optimization

有没有优化此代码(x86-64)的好方法?

mov dword ptr[rsp], 0;
mov dword ptr[rsp+4], 0
Run Code Online (Sandbox Code Playgroud)

其中立即数可以是任何值,不一定是零,但在这种情况下总是立即数。

原来的那对店还慢吗?硬件中的写入组合和 ?ops 的并行操作可能会使一切变得非常快?我想知道有没有问题可以解决。

我正在考虑类似的事情(不知道以下说明是否存在)

mov  qword ptr[rsp], 0
Run Code Online (Sandbox Code Playgroud)

或者

mov  eax, 0;
mov  qword ptr[rsp], rax    ; assuming we can spare a register, a bad idea to corrupt one though
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 7

是的,编译时/asm-source-level write-coalescing 通常是一个好主意,特别是如果两者或高半部分为零(或-1),那么您可以用 1 条指令完成整个 qword;现代 x86 CPU 具有高效的未对齐存储,尤其是当它不跨越缓存线边界时。

您通常希望最小化总融合域 uops(使您的代码尽可能高效地通过前端)、总代码大小(以字节为单位)和总未融合域 uops(调度程序 / RS 中的后端空间)。在这样的优先事项中。此外,Sandybridge 系列还有 uop-cache 考虑;64 位立即数或 32 位立即数 + disp8/disp32 可能需要从 uop 缓存行中的相邻条目借用额外空间。(请参阅https://agner.org/optimize/上的 Agner Fog 的 microarch pdf ,Sandybridge 章节。这仍然适用于 Skylake 等后来的 uarches)

此外,尽量减少对周围代码大量使用的某些后端执行端口的压力也是好的。Ice Lake 有 2 个存储数据和存储地址端口,因此可以并行运行两个存储,但在此之前,所有 x86 CPU 都被限制为每个时钟 1 个存储(只有一个存储数据端口将数据写入存储缓冲区) . 对 L1d 缓存的提交也仅限于存储缓冲区中每个时钟的 1 个存储。乱序 exec 确实使之平滑,因此 2 个背靠背存储不是大问题,但 2x 4 字节立即存储需要大量指令大小。

不幸的是mov r/m32, sign_extended_imm8,x86 没有,只有 imm32。( https://www.felixcloutier.com/x86/mov ) x86-64确实mov r/m64, sign_extended_imm32,不过,这是你应该使用的:

mov    qword [rsp], 0      ; 8 bytes, 1 fused-domain uop on modern Intel and AMD CPUs
Run Code Online (Sandbox Code Playgroud)

对7个字节+ 8个字节和2个微指令为mov dword [rsp],0/ mov dword [rsp+4], 0。异或归零 EAX / 存储 RAX 会更小(代码大小),但成本为 2 uop 而不是 1。

假设我们可以节省一个寄存器,但是破坏一个寄存器是个坏主意

几乎不; 您经常使用归零寄存器,而异或归零实际上与 Sandybridge-family 上的 NOP 一样便宜。(而且在 AMD 上也很便宜。)如果您可以在某个地方开设这家商店,以便将寄存器归零,这将非常便宜:

xor   eax, eax
mov  [rsp], rax         ; better if you have a use for RAX later
Run Code Online (Sandbox Code Playgroud)

或者对于您想要的非零 64 位值,mov r64, imm64通常您有一个备用寄存器可以用作临时目标。如果您必须在整个函数周围溢出一个寄存器或保存/恢复一个额外的 reg,那么如果您不能执行单个符号扩展 imm32,那么最好只执行 2 个单独的 dword-immediate 存储。


对于非零常量,如果整个 qword 常量可以表示为符号扩展的 32 位立即数,请使用mov qword [rsp], imm32. (或者push imm32更早地优化掉sub rsp, 8。)

如果您知道您的 qword 内存位置是 8 字节对齐的,那么即使对于不适合 32 位立即数的任意 8 字节常量,也值得组合:

mov  rax, 0x123456789abcdef0        ; 10 bytes, 1 uop
mov  [rsp], rax                     ; 4 bytes, 1 micro-fused uop, for port 4 + port 2,3, or 7
Run Code Online (Sandbox Code Playgroud)

它只比做 2 个单独的双字存储稍微好一些,并且在(可能?)罕见的情况下它跨越 64 字节缓存线边界可能会更慢

mov  dword [rsp], 0x9abcdef0        ; 7 bytes, 1 micro-fused uop for port 4 + port 2,3, or 7
mov  dword [rsp+4], 0x12345678      ; 8 bytes, 1 micro-fused uop for port 4 + port 2,3, or 7
Run Code Online (Sandbox Code Playgroud)

或者,如果您的常量恰好适合零扩展到 64 位但未进行符号扩展的 32 位值,您可以mov eax, 0x87654321(5 个字节,非常有效) / mov [rsp], rax


如果您想稍后进行 qword 重新加载,请务必进行单个 qword 存储,以便存储转发可以有效工作


硬件中的写组合

这不是主要因素。更重要的是 OoO exec 和存储缓冲区将存储执行与周围代码解耦。

如果你真正希望每个执行时钟获得超过1号店(任意宽度的),你肯定是出于运气冰湖前uarches。在任何 uarch(甚至非 x86)上,硬件存储合并发生在存储执行之后。

如果您希望它会合并并在存储缓冲区中占用更少的条目,那么您也很不走运,因此它有更多的时间/空间来吸收两个缓存未命中的存储。我们没有任何实际证据表明任何 x86 这样做来节省存储缓冲区消耗带宽,或更快地释放存储缓冲区条目。请参阅为什么退休后 RFO 不中断内存排序?对于我目前对(缺乏)存储缓冲区合并的理解。有一些证据表明,英特尔至少可以在缓存未命中存储上将存储未命中提交给 LFB,以释放存储缓冲区中的空间,但仅限于程序顺序的限制,并且没有证据表明每个时钟提交多个。


Bee*_*ope 5

是的,您可以将两个 32 位写入合并为一个 64 位写入,如下所示:

mov     QWORD PTR [rsp], 0
Run Code Online (Sandbox Code Playgroud)

立即数是一个 32 位符号扩展立即数,所以如果你的第二次写入是非零1,或者如果第一次写入的 MSB 是1,这不是那么简单。在这种情况下,你可以加载一个 64 位常量使用movabs和写。例如,要写出 1 和 2,

movabs  rax,  0x200000001
mov     QWORD PTR [rsp], rax
Run Code Online (Sandbox Code Playgroud)

常量0x200000001导致正确的值被写入每个 32 位的一半。

这个技巧对于零情况绝对值得,对于非零情况可能值得,而彼得的回答更详细地介绍了后一种情况下的权衡。

编译器也可以进行这种优化(他们称之为“存储组合”或类似的东西),这意味着你可以在 Godbolt 上使用它


1除非在特殊情况下,符号扩展名完全符合您的要求。即,第二个值正好0xFFFFFFFF是第一个值的高位。