想象一下如下所示的加载存储循环,它从非连续位置加载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
在现代的英特尔和AMD硬件上,当在高速缓存中运行时,这样的循环通常会在每个周期的一个商店中阻塞一个商店。这有点浪费,因为那只是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个uop - 每个QWORD
1.25个循环或每个DWORD
0.625个循环。
已经比第一个选项好多了,但我不禁认为这个改组有更好的选择 - 例如,我们通过使用普通负载浪费uop吞吐量 - 感觉我们应该能够至少结合一些ALU的操作与内存源操作数的负载,但我主要是在英特尔上受阻:内存上的shl
只有RMW形式,而shlx
和rolx
不会微融合。
似乎我们可以通过QWORD
使第二次加载-4
负载偏移来免费获得转换,但是然后我们在负载DWORD
中清除垃圾。
我对标量代码以及基本x86-64指令集和更好版本的代码感兴趣,如果可能的话,还有像BMI
这样有用的扩展。
看起来我们可以通过使第二次加载QWORD加载偏移量为-4来获得免费转换,但是我们将在加载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 iter,而每个时钟1个iter用于对齐。)如果16字节需要AVX版本故障加载过渡到未映射的页面,因此如果没有来自加载端口的额外支持,它不能仅使用更窄的加载。 :/
这样一个令人沮丧和无用的设计决策(大概是在加载端口之前可以免费零扩展,而不是用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上表现良好。