我的应用程序中有一个乘法添加内核,我想提高它的性能。
我使用英特尔酷睿i7-960(3.2 GHz时钟)并已使用SSE内在函数手动实现内核,如下所示:
for(int i=0; i<iterations; i+=4) {
y1 = _mm_set_ss(output[i]);
y2 = _mm_set_ss(output[i+1]);
y3 = _mm_set_ss(output[i+2]);
y4 = _mm_set_ss(output[i+3]);
for(k=0; k<ksize; k++){
for(l=0; l<ksize; l++){
w = _mm_set_ss(weight[i+k+l]);
x1 = _mm_set_ss(input[i+k+l]);
y1 = _mm_add_ss(y1,_mm_mul_ss(w,x1));
…
x4 = _mm_set_ss(input[i+k+l+3]);
y4 = _mm_add_ss(y4,_mm_mul_ss(w,x4));
}
}
_mm_store_ss(&output[i],y1);
_mm_store_ss(&output[i+1],y2);
_mm_store_ss(&output[i+2],y3);
_mm_store_ss(&output[i+3],y4);
}
我知道我可以使用压缩的fp向量来提高性能,我已经成功完成了,但我想知道为什么单个标量代码无法满足处理器的峰值性能。
这台内核在我的机器上的性能是每个周期约1.6个FP操作,而最大值是每个周期2个FP操作(因为FP add + FP mul可以并行执行)。
如果我对研究生成的汇编代码是正确的,那么理想的时间表将如下所示,其中mov
指令需要3个周期,从加载域到FP域的切换延迟用于相关指令需要2个周期,FP乘法需要4个周期,FP添加需要3个周期。 (请注意,乘法 - > add的依赖性不会导致任何切换延迟,因为操作属于同一个域。)
根据测量的性能(最大理论性能的约80%),每8个周期有大约3个指令的开销。
我正试图:
当然,缓存未命中存在问题。数据错位可能会增加移动指令的延迟,但是还有其他因素可以在这里发挥作用吗?就像寄存器读取档位一样?
我希望我的问题很清楚,先谢谢你的答复!
更新:内循环的程序集如下所示:
...
Block 21:
movssl (%rsi,%rdi,4), %xmm4
movssl (%rcx,%rdi,4), %xmm0
movssl 0x4(%rcx,%rdi,4), %xmm1
movssl 0x8(%rcx,%rdi,4), %xmm2
movssl 0xc(%rcx,%rdi,4), %xmm3
inc %rdi
mulss %xmm4, %xmm0
cmp $0x32, %rdi
mulss %xmm4, %xmm1
mulss %xmm4, %xmm2
mulss %xmm3, %xmm4
addss %xmm0, %xmm5
addss %xmm1, %xmm6
addss %xmm2, %xmm7
addss %xmm4, %xmm8
jl 0x401b52 <Block 21>
...
答案 0 :(得分:30)
我在评论中注意到:
但是,您的程序集会显示5条SSE movssl
指令。根据{{3}},Nehalem的所有浮点SSE移动指令至少为 1 inst / cycle 的倒数吞吐量。
由于你有5个,你做得比5个周期/迭代更好。
因此,为了达到最佳性能,您需要减少所拥有的负载数量。如何做到这一点我无法立即看到这个特例 - 但它可能是可能的。
一种常见方法是使用Agner Fog's tables。在哪里添加嵌套级别以改善局部性。虽然它主要用于改进缓存访问,但它也可以用在寄存器中以减少所需的加载/存储数量。
最终,您的目标是减少负载数量,使其少于add / muls的数量。所以这可能是要走的路。
答案 1 :(得分:1)
非常感谢您的回答,这解释了很多。 继续我的问题,当我使用压缩指令而不是标量指令时,使用内在函数的代码看起来非常相似:
for(int i=0; i<size; i+=16) {
y1 = _mm_load_ps(output[i]);
…
y4 = _mm_load_ps(output[i+12]);
for(k=0; k<ksize; k++){
for(l=0; l<ksize; l++){
w = _mm_set_ps1(weight[i+k+l]);
x1 = _mm_load_ps(input[i+k+l]);
y1 = _mm_add_ps(y1,_mm_mul_ps(w,x1));
…
x4 = _mm_load_ps(input[i+k+l+12]);
y4 = _mm_add_ps(y4,_mm_mul_ps(w,x4));
}
}
_mm_store_ps(&output[i],y1);
…
_mm_store_ps(&output[i+12],y4);
}
这个内核的测量性能是每个周期大约5.6个FP操作,尽管我认为它的性能正好是标量版本的4倍,即每个周期4.1,6 = 6,4 FP操作。
考虑到权重因素的移动(感谢指出这一点),时间表如下:
虽然在movss
操作之后有一条额外的指令将标量权重值移动到XMM寄存器,然后使用shufps
复制此标量值,但看起来调度没有改变在整个矢量中。似乎权重向量已准备好用于mulps
及时考虑从加载到浮点域的切换延迟,因此这不会产生任何额外的延迟。
movaps
(对齐,打包移动),addps
&amp;此内核中使用的mulps
指令(使用汇编代码检查)具有相同的延迟和时间。吞吐量作为它们的标量版本,所以这不应该产生任何额外的延迟。
是否有人知道每8个周期的额外周期花费在哪里,假设这个内核可以获得的最大性能是每个周期6.4个FP操作并且每个周期运行5.6个FP操作?
再次感谢您的帮助!
答案 2 :(得分:0)
从我的评论中得到答案。
在非服务器Linux发行版上我认为默认情况下中断计时器通常设置为250Hz,虽然它因发行版而异,但几乎总是超过150.这个速度是提供30 + fps交互式GUI所必需的。该中断计时器用于抢占代码。这意味着每秒150次以上的代码被中断,调度程序代码运行并决定提供更多时间的内容。听起来你只是获得80%的最大速度,没有任何问题。如果你需要更好的安装说,Ubuntu Server(100Hz默认)和调整内核(抢占关闭)
编辑:在一个2+核心系统上,这个影响要小得多,因为你的过程几乎肯定会被打到一个核心上,或多或少地留下来做自己的事情。