将两个 DWORD 打包成一个 QWORD 以节省存储带宽
Packing two DWORDs into a QWORD to save store bandwidth
想象一个如下所示的加载-存储循环,它从非连续位置加载 DWORD
s 并连续存储它们:
top:
mov eax, DWORD [rsi]
mov DWORD [rdi], eax
mov eax, DWORD [rdx]
mov DWORD [rdi + 4], eax
; unroll the above a few times
; increment rdi and rsi somehow
cmp ...
jne top
在现代 Intel 和 AMD 硬件上,当 运行 在缓存中时,这样的循环通常会在每个周期将 ones 存储在一个存储中。这有点浪费,因为那只是 2 的 IPC(一个商店,一个负载)。
自然而然出现的一个想法是将两个 DWORD
负载合并到一个 QWORD
存储中,这是可能的,因为这些存储是连续的。像这样的东西可以工作:
top:
mov eax, DWORD [rsi]
mov ebx, DWORD [rdx]
shl rbx, 32
or rax, rbx
mov QWORD [rdi]
基本上执行两个加载并使用两个 ALU 操作将它们组合成一个 QWORD
,我们可以用一个存储来存储它。现在我们在 uops 上遇到瓶颈:每 2 DWORD
s 5 uops - 所以每 QWORD
1.25 个周期或每 DWORD
.
0.625 个周期
已经比第一个选项好多了,但我忍不住认为对于这种改组有更好的选择 - 例如,我们通过使用普通加载来浪费 uop 吞吐量 - 它 感觉 就像我们应该能够将至少一些 ALU 操作与带有内存源操作数的负载结合起来,但我主要受阻于英特尔:shl
内存上只有 RMW 形式,并且 shlx
和 rolx
不要微熔断。
似乎我们可以通过将第二个负载设为 QWORD
偏移 -4
的负载来免费换班,但随后我们将清除负载中的垃圾 DWORD
.
我对标量代码感兴趣,以及基本 x86-64 指令集和更好版本的代码(如果可能的话)以及 BMI
.
等有用的扩展
It also seems like we could maybe get the shift for free by making the second load a QWORD load offset by -4, but then we are left clearing out garbage in the load DWORD.
如果更宽的负载对于正确性和性能来说是可以的(缓存行拆分...),我们可以使用 shld
top:
mov eax, DWORD [rsi]
mov rbx, QWORD [rdx-4] ; unaligned(?) 64-bit load
shld rax, rbx, 32 ; 1 uop on Intel SnB-family, 0.5c recip throughput
mov QWORD [rdi], rax
MMX punpckldq mm0, [mem]
SnB 系列(包括 Skylake)上的微型保险丝。
top:
movd mm0, DWORD [rsi]
punpckldq mm0, QWORD [rdx] ; 1 micro-fused uop on Intel SnB-family
movq QWORD [rdi], mm0
; required after the loop, making it only worth-while for long-running loops
emms
不幸的是,punpckl 指令有一个 向量宽度内存操作数,而不是半宽度 。这通常会破坏它们在其他情况下完美的用途(尤其是 16B 内存操作数必须对齐的 SSE2 版本)。但请注意,MMX 版本(只有一个 qword 内存操作数)没有对齐要求。
您也可以使用 128 位 AVX 版本,但这更有可能跨越缓存行边界并且速度很慢。 (Skylake 不会通过仅加载所需的 8 个字节来进行优化;具有对齐 mov
+ vpunckldq xmm1, xmm0, [cache_line-8]
的循环以每 2 个时钟运行 1 次迭代,而对齐时每时钟运行 1 次迭代。)AVX 版本是如果 16 字节加载跨入未映射的页面,则需要出错,因此它不能在没有加载端口额外支持的情况下仅使用较窄的加载。 :/
这样一个令人沮丧和无用的设计决定(大概是在加载端口可以免费零扩展之前做出的,并且没有用 AVX 修复)。至少我们有 movhps
作为内存源 punpcklqdq
的替代品,但实际随机播放的较窄宽度无法替代。
为避免 CL 分裂,您还可以使用单独的 movd
加载和 punpckldq
,或 SSE4.1 pinsrd
。有了这个,就没有理由要MMX了。
top:
movd xmm0, DWORD [rsi]
movd xmm1, DWORD [rdx] ; SSE2
punpckldq xmm0, xmm1
; or pinsrd xmm0, DWORD [rdx], 1 ; 2 uops not micro-fused
movq QWORD [rdi], xmm0
显然 AVX2 vpgatherdd
是一种可能性,并且可能在 Skylake 上表现良好。
想象一个如下所示的加载-存储循环,它从非连续位置加载 DWORD
s 并连续存储它们:
top:
mov eax, DWORD [rsi]
mov DWORD [rdi], eax
mov eax, DWORD [rdx]
mov DWORD [rdi + 4], eax
; unroll the above a few times
; increment rdi and rsi somehow
cmp ...
jne top
在现代 Intel 和 AMD 硬件上,当 运行 在缓存中时,这样的循环通常会在每个周期将 ones 存储在一个存储中。这有点浪费,因为那只是 2 的 IPC(一个商店,一个负载)。
自然而然出现的一个想法是将两个 DWORD
负载合并到一个 QWORD
存储中,这是可能的,因为这些存储是连续的。像这样的东西可以工作:
top:
mov eax, DWORD [rsi]
mov ebx, DWORD [rdx]
shl rbx, 32
or rax, rbx
mov QWORD [rdi]
基本上执行两个加载并使用两个 ALU 操作将它们组合成一个 QWORD
,我们可以用一个存储来存储它。现在我们在 uops 上遇到瓶颈:每 2 DWORD
s 5 uops - 所以每 QWORD
1.25 个周期或每 DWORD
.
已经比第一个选项好多了,但我忍不住认为对于这种改组有更好的选择 - 例如,我们通过使用普通加载来浪费 uop 吞吐量 - 它 感觉 就像我们应该能够将至少一些 ALU 操作与带有内存源操作数的负载结合起来,但我主要受阻于英特尔:shl
内存上只有 RMW 形式,并且 shlx
和 rolx
不要微熔断。
似乎我们可以通过将第二个负载设为 QWORD
偏移 -4
的负载来免费换班,但随后我们将清除负载中的垃圾 DWORD
.
我对标量代码感兴趣,以及基本 x86-64 指令集和更好版本的代码(如果可能的话)以及 BMI
.
It also seems like we could maybe get the shift for free by making the second load a QWORD load offset by -4, but then we are left clearing out garbage in the load DWORD.
如果更宽的负载对于正确性和性能来说是可以的(缓存行拆分...),我们可以使用 shld
top:
mov eax, DWORD [rsi]
mov rbx, QWORD [rdx-4] ; unaligned(?) 64-bit load
shld rax, rbx, 32 ; 1 uop on Intel SnB-family, 0.5c recip throughput
mov QWORD [rdi], rax
MMX punpckldq mm0, [mem]
SnB 系列(包括 Skylake)上的微型保险丝。
top:
movd mm0, DWORD [rsi]
punpckldq mm0, QWORD [rdx] ; 1 micro-fused uop on Intel SnB-family
movq QWORD [rdi], mm0
; required after the loop, making it only worth-while for long-running loops
emms
不幸的是,punpckl 指令有一个 向量宽度内存操作数,而不是半宽度 。这通常会破坏它们在其他情况下完美的用途(尤其是 16B 内存操作数必须对齐的 SSE2 版本)。但请注意,MMX 版本(只有一个 qword 内存操作数)没有对齐要求。
您也可以使用 128 位 AVX 版本,但这更有可能跨越缓存行边界并且速度很慢。 (Skylake 不会通过仅加载所需的 8 个字节来进行优化;具有对齐 mov
+ vpunckldq xmm1, xmm0, [cache_line-8]
的循环以每 2 个时钟运行 1 次迭代,而对齐时每时钟运行 1 次迭代。)AVX 版本是如果 16 字节加载跨入未映射的页面,则需要出错,因此它不能在没有加载端口额外支持的情况下仅使用较窄的加载。 :/
这样一个令人沮丧和无用的设计决定(大概是在加载端口可以免费零扩展之前做出的,并且没有用 AVX 修复)。至少我们有 movhps
作为内存源 punpcklqdq
的替代品,但实际随机播放的较窄宽度无法替代。
为避免 CL 分裂,您还可以使用单独的 movd
加载和 punpckldq
,或 SSE4.1 pinsrd
。有了这个,就没有理由要MMX了。
top:
movd xmm0, DWORD [rsi]
movd xmm1, DWORD [rdx] ; SSE2
punpckldq xmm0, xmm1
; or pinsrd xmm0, DWORD [rdx], 1 ; 2 uops not micro-fused
movq QWORD [rdi], xmm0
显然 AVX2 vpgatherdd
是一种可能性,并且可能在 Skylake 上表现良好。