2012-04-03 66 views
39

我的应用程序中有一个乘加内核,我想提高它的性能。C代码循环性能

我使用了Intel Core i7-960(3.2GHz的时钟),并已手动实现使用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个周期。 (请注意,乘法 - >添加的相关性不会导致任何切换延迟,因为这些操作属于同一个域)。

schedule

根据所测量的性能(〜最大理论性能的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

它确实依赖于很多编译器(甚至它的版本)以及您传递给它的优化标志。如果数值性能对您来说如此重要,您可能还需要花时间和精力学习数值库和/或OpenCL或CUDA(以利用GPGPU)。 Ther也是缓存考虑因素。在现在的处理器上预测循环的实际时间很困难。 – 2012-04-03 11:13:37

+4

我不明白为什么你会认为循环控制总是可以并行执行,而它实际上是在无序执行方案中创建完美的依赖关系链。 INC指令修改一个寄存器。 CMP指令必须等待INC完成以检查该寄存器中的值并相应地修改标志。然后,条件跳转指令必须等待CMP写入标志以决定是否实际跳转。恐怕没有平行化。更不要说跳跃会导致流水线延迟 - 分支预测器会处理这一点。 – 2012-04-03 11:13:39

+0

更不用说,INC指令必须等待修改标志的先前指令才能保持CF标志的状态。只需将INC替换为相应的ADD即可解决该问题。 – 2012-04-03 11:18:43

回答

29

我注意到,该意见:

  • 循环需要5个时钟周期执行。
  • “应该”需要4个周期。 (因为有4个加法和4个mulitplies)

但是,您的程序集显示5 SSE movssl说明。根据Agner Fog's tables所有浮点SSE移动指令至少为1次/周期 Nehalem的互惠吞吐量。

既然你有5个,你不能做比5个周期/迭代更好。


因此,为了达到最佳性能,您需要减少您拥有的负载数量。你怎么能做到这一点,我不能立即看到这个特殊情况 - 但它可能是可能的。

一种常用的方法是使用tiling。在哪里添加嵌套级别以改善局部性。虽然它主要用于改进缓存访问,但它也可用于寄存器中以减少所需的加载/存储数量。

最终,您的目标是减少负载的数量,使其小于添加/ muls的数量。所以这可能是要走的路。

+0

我还会提到整数SSE寄存器到寄存器的mov有3个inst /循环的吞吐量,但这是不相关的。所有加载/存储仍然是1次/周期。 – Mysticial 2012-04-03 17:04:03

+2

你怎么能在多任务系统上说这个?真的吗? 80%的理论吞吐量与Linux的桌面调度程序和上下文切换有关......我真的很想看看他是否可以通过1条指令减少循环,并获得更好的速度(使用不完整的内核) – 2012-04-03 18:00:18

+1

@OrgnlDave操作系统/内核开销通常比你想象的要小。根据我的经验,这可以忽略不计(<1%)。请参阅[此问题](http://stackoverflow.com/q/8389648/922184)以获得在Windows和Linux上达到峰值触发器97 +%的代码示例。 – Mysticial 2012-04-03 18:17:29

0

使这个从我的评论的答案。

在非服务器Linux发行版中,我相信中断计时器默认情况下通常设置为250Hz,虽然由于发行版的不同,它几乎总是超过150.这种速度对于提供30 fps交互式GUI是必需的。该中断计时器用于抢占代码。这意味着每秒150次以上的代码被中断,并且调度程序代码运行并决定要花更多时间。这听起来像你做得很好,只是获得最大速度的80%,没有问题。如果你需要更好的安装说,Ubuntu服务器(默认为100Hz),并调整内核(抢占关闭)

编辑:在2+核心系统,这个影响要小得多,因为你的进程几乎肯定会被打到一个核心和更多或更少的左派去做自己的事情。

+4

对不起,但这是无稽之谈。我能够测量linux系统上简单指令序列的处理器周期,抢占式和1kHz调度器。即使运行X,系统的开销通常也低于1%。另外,如果OP问题中的周期数从4到5恰好是由于开销 - 更自然的解释是该循环实际需要5个周期,这将是非常不可能的巧合。 – hirschhornsalz 2012-04-03 23:23:28

+0

@drhirsch我敢打赌你有两个核心。这是在对另一个问题的评论中提到的。我会编辑这个以反映这一点。 – 2012-04-03 23:27:58

+0

不改变事物。在运行n个测试程序实例时,我仍然可以进行相同的测量,其中n是核心数。 – hirschhornsalz 2012-04-03 23:34:16

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操作。

以权重因子的户口迁入(感谢指出了这一点),日程安排是这样的:

schedule

它看起来像计划不会改变,但有一个额外的指令在执行movss操作后将标量权值移至XMM寄存器,然后使用shufps将该标量值复制到整个矢量中。考虑到负载到浮点域的切换延迟时间,似乎权重矢量已准备好用于mulps,所以这不应该导致任何额外的延迟。这是在这个内核使用

movaps(排列,包装移动),addps & mulps指令(汇编代码核对)具有相同的延迟&吞吐量,其标版本,因此这不应该要么招致任何额外的延迟。

有没有人知道这个额外的周期每8个周期花费在哪里,假设这个内核可以获得的最大性能是每个周期6.4 FP操作并且每个周期运行在5.6 FP操作?

再次感谢您的帮助!

+1

我认为这是适合作为一个单独的问题。从现在开始,你有一个洗牌新问题。 (我现在没有看到答案)您可以将它链接回这个并声明它是一个延续。 – Mysticial 2012-04-04 07:58:55

+0

容易找出。确保权重向量不包含任何非规格化值。尝试没有shuffle指令的循环。它不会产生任何有用的结果,但也许你会发现哪个指令会花费你额外的周期(当然,我怀疑是洗牌)。 – hirschhornsalz 2012-04-04 08:23:32

+0

@drhirsch新问题在这里:http://stackoverflow.com/questions/10007243/c-code-loop-performance-continued因此,在那里重新发布您的评论。 – Mysticial 2012-04-04 08:33:35