优化慢循环

时间:2016-04-27 14:56:45

标签: c performance gcc optimization oprofile

代码看起来像这样,内部循环需要花费大量时间:

#define _table_derive                  ((double*)(Buffer_temp + offset))
#define Table_derive(m,nbol,pos)        _table_derive[(m) + 5*((pos) + _interval_derive_dIdQ * (nbol))]
char *Buffer_temp=malloc(...);

for (n_bol=0; n_bol<1400; n_bol++)  // long loop here
    [lots of code here, hundreds of lines with computations on doubles, other loops, etc]

    double ddI=0, ddQ=0;

    // This is the original code
    for(k=0; k< 100; k++ ) {
            ddI += Table_derive(2,n_bol,k);
            ddQ += Table_derive(3,n_bol,k);
    }
    ddI /= _interval_derive_dIdQ;
    ddQ /= _interval_derive_dIdQ;
    [more code here]
}

oprofile告诉我,大部分运行时都花在这里(第二列是时间的百分比):

129304  7.6913 :for(k=0; k< 100; k++) {
275831 16.4070 :ddI += Table_derive(2,n_bol,k);
764965 45.5018 :ddQ += Table_derive(3,n_bol,k);

我的第一个问题是:我可以依靠oprofile指示代码缓慢的适当位置(我尝试过-Og和-Ofast并且它基本相同)。

我的第二个问题是:为什么这个非常简单的循环比sqrt,atan2和之前的数百行计算慢?我知道我没有显示所有代码,但有很多代码,对我来说没有意义。

我尝试了各种优化器技巧来进行矢量化(没有工作)或展开(工作),但收益甚微,例如:

    typedef double aligned_double __attribute__((aligned(8)));
    typedef const aligned_double* SSE_PTR;
    SSE_PTR TD=(SSE_PTR)&Table_derive(2,n_bol,0);   // We KNOW the alignement is correct because offset is multiple of 8

    for(k=0; k< 100; k++, TD+=5) {
        #pragma Loop_Optimize Unroll No_Vector
        ddI += TD[0];
        ddQ += TD[1];
    }

我检查了优化器的输出: &#34; -Ofast -g -march = native -fopt-info-all = missed.info -funroll-loops&#34; 在这种情况下,我得到&#34;循环展开9次&#34;,但如果我尝试向量化,我得到(简而言之): &#34;不能强制对齐&#34;, &#34;矢量对齐可能无法到达&#34;, &#34;矢量化未对齐的访问&#34;, &#34;访问的未知对齐方式:*(prephitmp_3784 +((sizetype)_1328 +(long unsigned int)(n_bol_1173 * 500)* 2)* 4)&#34;

有什么方法可以加快速度吗?

附录: 感谢所有评论,我将尝试回答:

  • 是的,我知道代码是丑陋的(它不是我的代码),你还没有看到真正的原始代码(这是一个巨大的简化)
  • 我坚持使用这个数组,因为C代码在库中,大数组一旦被C处理和修改,就会传递给调用者(IDL,Python或C)。
  • 我知道使用一些结构而不是将char *转换为复杂的多维double *会更好,但请参见上文。当这个编程首次编写时,结构可能不是C规范的一部分(只是开玩笑......可能)
  • 我知道对于矢量化器来说,拥有数组结构比结构数组更好,但是,叹气......见上文。
  • 这是一个实际的外部循环(在调用程序中),因此这个单片阵列的总大小约为2Gb
  • 按原样运行大约需要15分钟,没有优化,一分钟后我重写了一些代码(更快的atan2,一些手册在数组内对齐......)我使用-Ofast和-march = native < / LI>
  • 由于硬件的约束变化,我试图更快地跟上数据流。
  • 我尝试使用Clang并且收益很小(几秒钟),但我没有看到获得优化报告的选项,例如-fopt-info。我是否必须将装配视为了解发生情况的唯一选择?
  • 系统是一个拥有500Gb内存的64核心,但是我还没有能够插入任何OpenMP pragma来并行化上面的代码(我已尝试过):它读取文件,解压缩它完全在内存中(2Gb),按顺序分析它(比如&#39; + =&#39;)并向调用IDL / Python吐出一些结果。所有都在一个核心上(但其他核心非常忙于实际的采集和后期处理)。 :(
  • 没用,谢谢你的优秀建议:删除ddQ + = ...似乎将时间百分比转移到上一行:376280 39.4835:ddI + = ...
  • 这让我们变得更好:删除两个(因此整个循环)节省......什么都没有!所以我猜彼得说,我不相信剖析器。如果我描述无环编程,我会更均匀地分布时间(之前3行仅在1s以上,现在大约10,所有无意义的,如简单变量分配)。

我猜这个内环从一开始就是红鲱鱼;我将使用手动计时重新启动我的优化。感谢。

1 个答案:

答案 0 :(得分:3)

  

我的第一个问题是:我可以依靠oprofile表示正确的   代码缓慢的地方

不准确。根据我的理解,循环通常会对等待输入(或其他执行资源)的指令负责,而不是产生输入缓慢或释放任何其他执行资源的指令。

但是,在您的oprofile输出中,它可能实际上是最终循环。这个外环内还有其他内环吗?

您是否了解缓存未命中?除了周期之外,还有很多有趣的东西。

另请注意,要真正了解性能,您需要查看asm上的配置文件注释,而不是C.奇怪的是,一个人添加的时间比另一个多,但这可能只是将insn映射到源代码行的问题。

re:来自评论循环的结果:

所以没有那个内循环,程序根本没有运行得更快?如果外部循环已经触及了那个内存,那么你可能只是在缓存未命中时遇到瓶颈,内部循环只是再次触及那个内存?尝试perf record -e L1-dcache-load-misses ./a.out然后perf report。或oprofile等价物。

也许内循环uops等待发出,直到外循环中的慢速东西退出。现代英特尔CPU中的ReOrder缓冲区(ROB)大小约为200微秒,并且大多数默认解码为单个uop,因此无序窗口大约有200条指令。

注释掉内循环也意味着外循环中任何循环携带的依赖链在内循环运行时没有时间完成。删除内部循环可能会导致外部循环瓶颈的质量变化,从吞吐量到延迟。

re:使用-Ofast -march=native快15倍。好的,这很好。未经优化的代码可怕,不应被视为任何类型的&#34;基线&#34;或任何表现。如果您想与某些内容进行比较,请与-O2进行比较(不包括自动向量化,-ffast-math-march=native)。

尝试使用-fprofile-generate / -fprofile-use。 profile-use包含-funroll-loops,因此我假设当有可用的分析数据时,该选项效果最佳。

re:自动并行化:

您必须具体使用OpenMP pragma或-floop-parallelize-all -ftree-parallelize-loops=4 -fprofile-use启用它。如果存在非平凡的循环携带依赖性,则可能无法进行自动并行化。该维基页面也很旧,并且可能无法反映自动并行化的最新技术。我认为OpenMP暗示哪些循环并行化比编译器猜测更合理,特别是。没有clang -Rpass=inline

  

我尝试使用Clang并且收益很小(几秒钟),但我没有看到获得优化报告的选项,例如-fopt-info。我是否必须将装配视为了解发生情况的唯一选择?

gcc options您可以使用loop-vectorize获取有关内联的报告。 clang manual says向量化传递的名称为-Rpass-missed=loop-vectorize,因此您可以使用-Rpass-analysis=loop-vectorizefor(k=0; k< 100; k++ ) { ddI += Table_derive(2,n_bol,k); ddQ += Table_derive(3,n_bol,k); } 告诉您哪个语句导致矢量化失败。

查看asm是了解它是否自动向量化是否的唯一方法,但要真正判断编译器的工作,你必须知道如何自己编写高效的asm (所以你大致知道它可以做些什么。)请参阅The llvm docs say标签wiki中的http://agner.org/optimize/和其他链接。

我没有尝试将您的代码放在上以便使用不同的编译器进行尝试,但如果您的示例使得asm代表您从完整版中看到的内容,则可以发布链接源。

自动矢量

double

这应该自动矢量化,因为2和3是连续元素。如果将表拆分为多个表,则可以获得更好的缓存局部性(对于此部分)。例如将每组5的元素2和3保持在一个数组中。将一起使用的其他元素分组到表中。 (如果有重叠,例如另一个循环需要元素1和3,那么可能会拆分无法自动向量化的那个?)

re:问题更新:你不需要一个数组结构来自动向量化SSE。 16B向量恰好包含两个[ ddI ddQ ] s,因此编译器可以使用addsd累积vmovupd的向量。对于AVX 256b向量,它必须执行vinsertf128 / double才能从相邻结构中获取double对,而不是单个256b负载,但不是很大应对。但是,记忆位置是一个问题;您只使用了触摸的缓存行中每5 -ffast-math个中的2个。

即使没有-msse2,它也应该自动矢量化,只要您使用双精度矢量定位CPU即可。 (例如x86-64,或带double的32位)。

gcc喜欢使用标量为可能未对齐的数据做大的序言,直到达到对齐的地址。这会导致代码膨胀,尤其是256b向量和小元素。但是char不应该太糟糕了。仍然,尝试clang 3.7或clang 3.8。 clang使用未对齐的加载自动向量化可能未对齐的访问,在运行时对齐数据时没有额外的成本。 (gcc针对数据未对齐的希望极少的情况进行了优化,因为即使在对齐数据上使用时,旧CPU(例如Intel pre-Nehalem)上的未对齐加载/存储指令也较慢。)

如果double阵列无法证明每个double甚至是8B对齐的,那么struct阵列可能会击败自动矢量图。就像@JohnBollinger评论的那样,这真的很难看。如果你有5个双打的结构数组,那就这样声明!

如何将其编写为结构数组:

保留&#34;手册&#34;多维索引,但使基本1D数组成为double或更好Buffer_temp类型的数组,因此编译器将假设每个char*都是8B对齐的。

对于每次访问数组,您的原始版本也引用了全局double*。 (或者它是本地的吗?)任何可能对其进行别名的商店都需要重新加载基本指针。 (C&#39的别名规则允许typedef struct table_derive_entry { double a,b,c,d,e; } derive_t; void foo(void) { // I wasn't clear on whether table is static/global, or per-call scratch space. derive_t *table = aligned_alloc(foo*bar*sizeof(derive_t), 64); // or just malloc, or C99 variable size array. // table += offset/sizeof(table[0]); // if table is global and offset is fixed within one call... // maybe make offset a macro arg, too? #define Table_derive(nbol, pos) table[offset/sizeof(derive_t) + (pos) + _interval_derive_dIdQ / sizeof(derive_t) * (nbol))] // ... for(k=0; k< 100; k++ ) { ddI += Table_derive(n_bol, k).b; ddQ += Table_derive(n_bol, k).c; } // ... } #undef Table_derive 为别名设置别名,但我认为在解除引用之前你的转换为_interval_derive_dIdQ可以使你保存。你不会存储到内部的数组中无论如何循环,但我假设你在外部数组。)

offset

如果double *table = ...;#define Table_derive(nbol, pos) ( ((derive_t *)(double_table + offset/sizeof(double) + _interval_derive_dIdQ / sizeof(double) * (nbol)))[pos] ) 不是5 * 8B的倍数,那么您可能需要声明ddI /= _interval_derive_dIdQ; ddQ /= _interval_derive_dIdQ; 并将Table_derive修改为类似

double inv_interval_derive_dIdQ = 1.0 / _interval_derive_dIdQ;

FP部门:

{{1}}

你可以将{{1}}提升出来吗?乘法比分数便宜得多,尤其是。如果延迟很重要或者sqrt也需要div单位。