是否有优化此代码(x86-64)的好方法?
mov dword ptr[rsp], 0;
mov dword ptr[rsp+4], 0
其中立即值可以是任何值,不一定是零,但在这种情况下总是立即常数。
原来的一对商店甚至很慢吗?硬件中的写组合和μops的并行操作可能会让一切都快得离谱?我想知道是否没有问题需要解决。
我在想类似的东西(不知道下面的说明是否存在)
mov qword ptr[rsp], 0
或
mov eax, 0;
mov qword ptr[rsp], rax ; assuming we can spare a register, a bad idea to corrupt one though
是的,编译时/asm源代码级别的写合并通常是个好主意,尤其是如果两者或高半部分都为零(或-1
),那么您可以用1条指令完成整个qword;现代x86 CPU具有高效的未对齐存储,尤其是当它不跨越缓存线边界时。
您通常希望最小化总的融合域uop(以使代码尽可能高效地通过前端)、总的代码大小(以字节为单位)和总的未融合域uops(调度器/RS中的后端空间)。在类似的优先级中。此外,Sandybridge家族还需要考虑uop缓存;64位立即数,或32位立即数+disp8/disp32,可能需要从uop缓存行中的相邻条目借用额外空间。(参见Agner Fog的微阵列pdfhttps://agner.org/optimize/,Sandybridge章节。这仍然适用于后来的uarche(如Skylake)
同时,将周围代码大量使用的某些后端执行端口的压力降到最低也是很好的。Ice Lake有2个存储数据和存储地址端口,因此可以并行运行这两个存储,但在此之前,所有x86 CPU每个时钟只能有1个存储(只有一个存储数据端口可以将数据写入存储缓冲区)。提交到L1d缓存也被限制为来自存储缓冲区的每个时钟1个存储。无序的exec确实平滑了这一点,所以2个背靠背的存储不是什么大问题,但2个4字节的立即存储占用了大量的指令大小。
遗憾的是,x86没有mov r/m32, sign_extended_imm8
,只有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
与。对于CCD_ 4/CCD_。xor归零EAX/存储RAX将更小(代码大小),但成本为2 uops而不是1。
假设我们可以腾出一个寄存器,通过破坏一个寄存器是个坏主意
几乎;你经常使用归零寄存器,而xor归零实际上和Sandybridge家族的NOP一样便宜。(在AMD上也很便宜。)如果你能在某个地方做这个商店,让零寄存器变得有用,这是非常便宜的:
xor eax, eax
mov [rsp], rax ; better if you have a use for RAX later
或者,对于需要mov r64, imm64
的非零64位值,通常您有一个可用作临时目标的备用寄存器。如果你必须溢出一个寄存器或在整个函数周围保存/恢复一个额外的reg,那么如果你不能执行单个sign-extended-imm32,那么最好只执行两个单独的双字立即存储。
对于非零常数,如果整个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
它只比做两个单独的双字存储好一点,而且在(可能?)罕见的情况下,它跨越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和存储缓冲区将存储执行与周围代码解耦。
如果你实际上希望每个时钟执行一个以上的存储(任何宽度),那么你在冰湖之前的uarche上肯定运气不佳。在任何uarch(甚至是非x86)上,硬件存储区合并都发生在存储区执行之后。
如果你希望它能合并并在存储缓冲区中占用更少的条目,这样它就有更多的时间/空间来吸收两个缓存未命中的存储,那么你也就不走运了。我们没有任何真正的证据表明任何x86可以这样做来节省存储缓冲区消耗带宽,或者更快地释放存储缓冲区条目。请参阅为什么不';退役后的RFO是否会中断记忆订购?对于我目前对存储缓冲区合并的理解(缺乏)。有证据表明,英特尔至少可以将存储未命中提交给缓存未命中存储上的LFB,以释放存储缓冲区中的空间,但仅限于程序顺序的限制,并且没有证据表明每个时钟提交多个。
是的,您可以将两个32位写入合并为一个64位写入,如下所示:
mov QWORD PTR [rsp], 0
立即数值是一个32位符号扩展的立即数,所以如果第二次写入为非零1,或者第一次写入的MSB为1,就不是那么简单了。在这种情况下,您可以使用movabs
加载一个64位常量并进行写入。例如,要写入1和2,
movabs rax, 0x200000001
mov QWORD PTR [rsp], rax
常数CCD_ 13导致将正确的值写入每个32位的一半。
对于零的情况,这个技巧肯定是值得的,对于非零的情况可能也是值得的,Peter的回答更详细地介绍了后一种情况下的权衡。
编译器也可以进行这种优化(他们称之为"存储组合"或类似的东西),这意味着你可以在godbolt上使用它。
1除非在特殊情况下,符号扩展可以满足您的要求。即,第二个值恰好是0xFFFFFFFF
,并且设置了第一个值的高位。