C代码循环性能
我的应用程序中有一个乘加内核,我想提高它的性能。
我使用Intel Core 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 + FP mul可以并行执行)。
如果我正确地研究生成的汇编代码,那么理想的调度将如下所示,其中mov
指令需要3个周期,相关指令从加载域到FP域的切换延迟需要2个周期,FP乘法需要4个周期,FP添加需要3个周期。 (请注意,从multiply – > add的依赖性不会导致任何切换延迟,因为这些操作属于同一个域)。
根据测量的性能(最大理论性能的〜80%),每8个周期有〜3条指令的开销。
我正在试图:
- 摆脱这个开销,或者
- 解释它来自哪里
当然,高速caching未命中和数据错位可能会增加移动指令的等待时间,但是在这里还有其他什么因素可以发挥作用吗? 像寄存器读取摊位什么的?
我希望我的问题是清楚的,在此先感谢您的答复!
更新:内部循环的程序集如下所示:
... 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> ...
我在评论中注意到:
- 循环需要5个周期来执行。
- “应该”需要4个周期。 (因为有4增加和4 mulitplies)
但是,您的程序集显示了5个SSE movssl
指令。 根据Agner Fog的表格,对于Nehalem来说,所有的浮动SSE移动指令至less为1次/周期的相互吞吐量。
既然你有5个, 你不可能比5个周期/迭代更好 。
因此,为了达到最佳性能,您需要减less您拥有的负载量。 你怎么能做到这一点,我不能立即看到这个特定的情况 – 但它可能是可能的。
一种常见的方法是使用平铺 。 在哪里添加嵌套级别来改善局部性。 虽然它主要用于改善caching访问,但也可以用在寄存器中以减less所需的加载/存储数量。
最终,你的目标是减less负载的数量less于add / muls的数量。 所以这可能是要走的路。
非常感谢你的答案,这很多解释。 继续我的问题,当我使用打包指令,而不是标量指令使用内在函数的代码看起来非常相似:
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); }
这个内核的testing性能是每个周期大约5.6个FP操作,但是我认为它恰好是标量版本的4倍,即4.1,6 = 6,4个FP操作每个周期。
考虑到重量因素的移动(感谢指出),时间表看起来像:
它看起来像日程安排不会改变,虽然在movss
操作之后有一个额外的指令,将标量权值移到XMM寄存器,然后使用shufps
在整个vector中复制这个标量值。 考虑到从负载到浮点域的切换延迟时间,似乎权重vector已经准备好用于mulps
,所以这不应该引起任何额外的延迟。
在这个内核中使用的movaps
(alignment的,压缩的移动), addps
& mulps
指令(用汇编代码检查)与标量版本具有相同的延迟和吞吐量,所以这不应该引起任何额外的延迟。
假设这个内核可以获得的最高性能是每个周期6.4 FP运算,并且每个周期运行在5.6 FP运算?
再次感谢您的帮助!
从我的评论做出这个答案。
在一个非服务器的Linux发行版中,我相信中断计时器通常默认设置为250Hz,不过这个数字会随着发行版的不同而变化,但总是超过150.这个速度对于提供一个30 + fps的交互式graphics用户界面是非常必要的。 该中断定时器用于抢占代码。 这意味着每秒150次以上的代码被中断,并且调度程序代码运行,并决定给予更多时间。 这听起来像你做得很好,只是得到最大速度的80%,没有问题。 如果你需要更好的安装说,Ubuntu服务器(100Hz的默认),并调整内核(抢先closures)一点
编辑:在一个2+核心系统上,这个系统的影响要小得多,因为你的过程几乎肯定会被放在一个核心上,或多或less地留下来做自己的事情。