英特尔处理器可以发出两个浮点运算,但每个周期一个负载,因此内存访问是最严格的约束。考虑到这一点,我首先打算使用打包加载来减少加载指令的数量,并使用打包算术只是因为它方便。从那以后,我意识到内存带宽饱和可能是最大的问题,如果重点是让代码运行得更快而不是学习矢量化,那么所有与 SSE 指令有关的混乱可能都是过早的优化。
上海证券交易所
在不假设b 中的索引的情况下,尽可能少的负载需要展开循环四次。一个 128 位加载从 b 获得四个索引,两个 128 位加载每个从 c 获得一对相邻的双精度数,收集 a 需要独立的 64 位加载。
对于串行代码,每四次迭代需要 7 个周期。 (如果对a 的访问不能很好地缓存,足以使我的内存带宽饱和)。我省略了一些烦人的事情,比如处理不是 4 的倍数的迭代次数。
entry: ; (rdi,rsi,rdx,rcx) are (n,a,b,c)
xorpd xmm0, xmm0
xor r8, r8
loop:
movdqa xmm1, [rdx+4*r8]
movapd xmm2, [rcx+8*r8]
movapd xmm3, [rcx+8*r8+8]
movd r9, xmm1
movq r10, xmm1
movsd xmm4, [rsi+8*r9]
shr r10, 32
movhpd xmm4, [rsi+8*r10]
punpckhqdq xmm1, xmm1
movd r9, xmm1
movq r10, xmm1
movsd xmm5, [rsi+8*r9]
shr r10, 32
movhpd xmm5, [rsi+8*r10]
add r8, 4
cmp r8, rdi
mulpd xmm2, xmm4
mulpd xmm3, xmm5
addpd xmm0, xmm2
addpd xmm0, xmm3
jl loop
取出索引是最复杂的部分。 movdqa 从一个 16 字节对齐的地址加载 128 位整数数据(Nehalem 会因混合“整数”和“浮点”SSE 指令而产生延迟惩罚)。 punpckhqdq 将高 64 位移动到低 64 位,但在整数模式下,与更简单的名称 movhlpd 不同。 32 位移位在通用寄存器中完成。 movhpd 将一个 double 加载到 xmm 寄存器的上部而不影响下部 - 这用于将 a 的元素直接加载到打包寄存器中。
这段代码明显比上面的代码快,而上面的代码又比简单的代码快,而且在每个访问模式上,除了简单的情况B[i] = i,其中天真的循环实际上是最快的。我还尝试了一些东西,比如 Fortran 中 SUM(A(B(:)),C(:)) 周围的函数,它最终基本上等同于简单循环。
我在具有 4GB DDR2-667 内存、4 个模块的 Q6600(2.4Ghz 的 65 nm Core 2)上进行了测试。
测试内存带宽大约为 5333 MB/s,所以看起来我只看到一个通道。我正在使用 Debian 的 gcc 4.3.2-1.1、-O3 -Ffast-math -msse2 -Ftree-vectorize -std=gnu99 进行编译。
为了测试,我让n 为一百万,初始化数组,使a[b[i]] 和c[i] 都等于1.0/(i+1),并使用几种不同的索引模式。一个为a分配一百万个元素并将b设置为随机排列,另一个为a分配10M个元素并每10个使用一次,最后一个为a分配10M个元素并通过添加设置b[i+1]一个从 1 到 9 到 b[i] 的随机数。我正在计时使用gettimeofday 调用一次需要多长时间,通过在数组上调用clflush 来清除缓存,并测量每个函数的1000 次试验。我使用来自criterion 的一些代码(特别是statistics 包中的内核密度估计器)绘制了平滑的运行时分布。
带宽
现在,关于带宽的实际重要说明。 5333MB/s 和 2.4Ghz 时钟刚好超过每个周期两个字节。我的数据足够长,任何东西都不应该被缓存,如果一切都未命中,将循环的运行时间乘以每次迭代加载的 (16+2*16+4*64) 字节,几乎可以得到我的系统拥有的 ~5333MB/s 带宽.如果没有 SSE,应该很容易使带宽饱和。即使假设a 被完全缓存,只需读取b 和c 进行一次迭代就会移动12 个字节的数据,并且天真的可以通过流水线在第三个周期开始新的迭代。
假设在a 上没有完全缓存,那么算术和指令计数就不会成为瓶颈。如果我的代码中的大部分加速来自于向b 和c 发出更少的负载,我不会感到惊讶,因此可以有更多空间来跟踪和推测a 上过去的缓存未命中。
更广泛的硬件可能会产生更大的影响。运行三个 DDR3-1333 通道的 Nehalem 系统需要每个周期移动 3*10667/2.66 = 12.6 字节以使内存带宽饱和。如果a 适合缓存,这对于单个线程来说是不可能的 - 但是在 64 字节时,向量上的行缓存未命中会迅速加起来 - 缓存中缺少的循环中的四个负载中的一个将平均所需带宽提高到16 字节/周期。