是的,编译时/asm源代码级写入合并通常是一个好主意,特别是如果两者或高半部分为零(或-1
)这样你就可以用 1 条指令完成整个 qword;现代 x86 CPU 具有高效的未对齐存储,尤其是当它不跨越缓存行边界时。
您通常希望最小化总融合域微指令(以使代码尽可能高效地通过前端)、总代码大小(以字节为单位)以及总未融合域微指令(调度程序/RS 中的后端空间)。在类似的优先事项中。此外,Sandybridge 系列还需要考虑 uop 缓存; 64 位立即数或 32 位立即数 + disp8/disp32 可能需要从 uop 缓存行中的相邻条目借用额外空间。 (参见 Agner Fog 的 microarch pdfhttps://agner.org/optimize/ https://agner.org/optimize/,桑迪布里奇章节。这仍然适用于像 Skylake 这样的后来的 uarch)
此外,最大限度地减少周围代码大量使用的某些后端执行端口的压力也是有好处的。 Ice Lake 有 2 个存储数据端口和存储地址端口,因此可以并行运行两个存储,但在此之前,所有 x86 CPU 仅限每个时钟 1 个存储(只有一个存储数据端口用于将数据写入存储缓冲区) 。对 L1d 高速缓存的提交也仅限于每个时钟从存储缓冲区进行 1 次存储。乱序执行确实可以解决这个问题,因此 2 个连续存储并不是一个大问题,但 2x 4 字节立即存储需要大量指令大小。
不幸的是 x86 没有mov r/m32, sign_extended_imm8
,仅imm32。 (https://www.felixcloutier.com/x86/mov https://www.felixcloutier.com/x86/mov)x86-64does have mov r/m64, sign_extended_imm32
不过,您应该使用以下内容:
mov qword [rsp], 0 ; 8 bytes, 1 fused-domain uop on modern Intel and AMD CPUs
与 7 个字节 + 8 个字节和 2 个微指令mov dword [rsp],0
/ mov dword [rsp+4], 0
。异或归零 EAX / 存储 RAX 会更小(代码大小),但成本为 2 uop,而不是 1。
假设我们可以腾出一个寄存器,但破坏寄存器是个坏主意
几乎不;您经常需要使用归零寄存器,并且异或归零实际上与 Sandybridge 系列上的 NOP 一样便宜 https://stackoverflow.com/questions/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and。 (而且在 AMD 上也很便宜。)如果您可以在某个地方进行此存储,从而使归零寄存器变得有用,那么这是非常便宜的:
xor eax, eax
mov [rsp], rax ; better if you have a use for RAX later
或者对于您想要的非零 64 位值mov r64, imm64
,通常您有一个备用寄存器可以用作暂存目标。如果您必须在整个函数周围溢出寄存器或保存/恢复额外的寄存器,那么如果您无法执行单个符号扩展imm32,则最好只执行两个单独的双字立即存储。
对于非零常量,如果整个 qword 常量可以表示为符号扩展的 32 位立即数,则使用mov qword [rsp], imm32
. (Or 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
它只比执行 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
或者如果您的常量恰好适合 32 位值零扩展到 64 位,但不进行符号扩展,您可以mov eax, 0x87654321
(5个字节,非常高效)/mov [rsp], rax
.
如果您想稍后进行 qword 重新加载,请务必执行单个 qword 存储,以便存储转发可以有效地工作.
硬件中的写组合
这不是主要因素。更重要的是 OoO exec 和存储缓冲区将存储执行与周围代码解耦。
If you were实际上希望每个时钟执行时获得超过 1 个存储(任何宽度),在 Ice Lake 之前你在 uarches 上肯定不走运。在任何 uarch(甚至非 x86)上,硬件存储合并会在存储执行后发生。
如果您希望它能够合并并在存储缓冲区中获取更少的条目,以便它有更多的时间/空间来吸收两个缓存未命中存储,那么您也不走运。我们没有任何真正的证据表明任何 x86 这样做是为了节省存储缓冲区耗尽带宽,或者更快地释放存储缓冲区条目。看为什么退休后的 RFO 不会破坏内存排序? https://stackoverflow.com/questions/62376976/why-doesnt-rfo-after-retirement-break-memory-ordering以我目前对(缺乏)存储缓冲区合并的理解。有一些证据表明,英特尔至少可以将存储未命中提交给高速缓存未命中存储上的 LFB,以释放存储缓冲区中的空间,但仅限于程序顺序的限制,并且没有证据表明每个时钟提交多个。