首页 文章

C代码循环性能

提问于
浏览
40

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

我使用英特尔酷睿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的依赖性不会导致任何切换延迟,因为操作属于同一个域) .

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> 
...

3 回答

  • 1

    我在评论中注意到:

    • 循环需要5个周期才能执行 .

    • 它's 997763 to take 4 cycles. (since there' s 4添加和4 mulitplies)

    但是,您的程序集显示5条SSE movssl 指令 . 根据Agner Fog's tables,所有浮点SSE移动指令至少是Nehalem的 1 inst/cycle 倒数吞吐量 .

    由于你有5个, you can't do better than 5 cycles/iteration .


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

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

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

  • 0

    非常感谢您的回答,这解释了很多 . 继续我的问题,当我使用压缩指令而不是标量指令时,使用内在函数的代码看起来非常相似:

    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 来复制整个向量中的这个标量值,但看起来计划并没有改变 . 似乎权重向量已经准备好用于及时考虑从加载到浮点域的切换延迟,因此这不应该产生任何额外的延迟 .

    在此内核中使用的 movaps (对齐,打包移动), addpsmulps 指令(使用汇编代码检查)具有与其标量版本相同的延迟和吞吐量,因此这不应产生任何额外延迟 .

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

    再次感谢您的帮助!

  • 30

    从我的评论中得到答案 .

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

    编辑:在一个2核心系统上,这个影响要小得多,因为你的过程几乎肯定会被打到一个核心上,或多或少左边做自己的事情 .

相关问题