想象一下如下所示的加载存储循环,它从非连续位置加载DWORD
并连续存储它们:
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
个5 uop - 所以每QWORD
个1.25个周期或每DWORD
个0.625个周期。
已经比第一个选项好多了,但我不禁认为这个改组有更好的选择 - 例如,我们通过使用普通负载浪费uop吞吐量 - 它感觉就像我们应该能够将至少一些ALU操作与具有内存源操作数的负载组合在一起,但我在英特尔上大多受到阻碍:内存上的shl
只有RMW形式,shlx
和rolx
不要微融合。
似乎我们可以通过使QWORD
加载-4
负载偏移DWORD
来获得免费转换,但是我们将在加载中清除垃圾{{1} }。
我对基本x86-64指令集和更好版本的标量代码和代码感兴趣,如果可能的话,使用BMI
等有用的扩展。
答案 0 :(得分:4)
似乎我们可以通过使第二次加载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。)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上表现良好。