43

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

我使用 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 add + FP mul 可以并行执行)。

如果我对生成的汇编代码的研究是正确的,那么理想的时间表如下所示,其中mov指令需要 3 个周期,依赖指令从加载域到 FP 域的切换延迟需要 2 个周期,FP 乘法需要4 个周期,FP add 需要 3 个周期。(请注意,来自乘法 -> 加法的依赖不会产生任何切换延迟,因为这些操作属于同一个域)。

日程

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

3 回答 3

30

我在评论中注意到:

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

但是,您的程序集显示 5 个 SSEmovssl指令。根据Agner Fog 的表格,所有浮点 SSE 移动指令对于 Nehalem 来说至少是1 inst/cycle倒数吞吐量。

因为你有 5 个,所以你不能做得比 5 个周期/迭代更好


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

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

最终,您的目标是将负载数量减少到少于 add/muls 的数量。所以这可能是要走的路。

于 2012-04-03T16:55:20.383 回答
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(对齐的、打包的移动)addpsmulps指令(用汇编代码检查)具有与其标量版本相同的延迟和吞吐量,因此这也不应该产生任何额外的延迟。

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

再次感谢您的所有帮助!

于 2012-04-04T07:41:12.360 回答
0

从我的评论中得出这个答案。

在非服务器 Linux 发行版上,我相信默认情况下中断计时器通常设置为 250Hz,尽管因发行版而异,它几乎总是超过 150。该速度对于提供 30+fps 交互式 GUI 是必需的。该中断计时器用于抢占代码。这意味着每秒 150 多次您的代码被中断,调度程序代码运行并决定给什么更多时间。听起来你做得很好,只需获得最大速度的 80%,那里没有问题。如果您需要更好的安装,比如 Ubuntu Server(默认 100Hz)并稍微调整内核(关闭抢占)

编辑:在 2+ 核心系统上,这影响要小得多,因为您的流程几乎肯定会被打到一个核心上,并且或多或少地留给自己做事。

于 2012-04-03T16:28:51.680 回答