如何优化简单的循环?

时间:2015-12-22 12:06:32

标签: c++ optimization vectorization multicore simd

循环很简单

void loop(int n, double* a, double const* b)
{
#pragma ivdep
    for (int i = 0; i < n; ++i, ++a, ++b)
        *a *= *b;
}

我正在使用intel c ++编译器并使用#pragma ivdep进行优化。有什么方法可以让它像一起使用多核和矢量化或其他技术一样表现得更好?

4 个答案:

答案 0 :(得分:1)

假设a指向的数据不能与b指向的数据重叠,那么让编译器优化代码的最重要信息就是这个事实。

在较旧的ICC版本中,“restrict”是向编译器提供该关键信息的唯一简洁方法。在较新的版本中,有一些更清晰的方法可以提供比ivdep给出的更强大的保证(实际上ivdep对优化器的承诺比它看起来更弱并且通常没有预期的效果) 。

但如果n很大,整个事情将由缓存未命中控制,因此没有本地优化可以帮助。

答案 1 :(得分:1)

  1. 此循环绝对是编译器的。但要确保循环实际上是矢量化的(使用编译器&#39; -qopt-report5,汇编输出,Intel (vectorization) Advisor,无论其他技术如何)。另一种过度的方法是使用-no-vec选项创建性能基线(这将禁用ivdep驱动和自动向量化),然后将执行时间与它进行比较。这不是检查矢量化存在的好方法,但它对下一个子弹的一般性能分析很有用。
  2. 如果循环没有实际矢量化,请确保推送编译器自动矢量化它。为了推动编译器,请参阅下一个子弹。请注意,即使循环成功自动矢量化,下一个项目符号也很有用。

    1. 要推动编译器进行矢量化,请使用:(a)限制关键字以消除&#34;消除歧义&#34; a和b指针(有人已经建议你)。 (b) #pragma omp simd (它比ivdep更具可移植性和灵活性,但也有一个缺点,即在英特尔编译器版本14和其他循环之前在旧编译器中不受支持更多&#34;危险&#34;)。重新强调:给定的子弹似乎与ivdep做同样的事情,但根据各种情况,它可能是更好,更强大的选择。

    2. 给定循环具有细粒度迭代(每次迭代的计算量太少)并且总体上不是纯粹的计算限制(因此CPU花费/用于从/向缓存/存储器加载/存储数据的周期如果不是更大的努力/周期花费来执行乘法是可比较的)。展开通常是稍微减轻这些缺点的好方法。但是我建议明确要求编译器使用 #pragma unroll 来展开它。实际上,对于某些编译器版本,展开将自动进行。同样,您可以使用-qopt-report5,循环汇编或英特尔(矢量化)Advisor检查编译器何时执行此操作: enter image description here

    3. 在给定的循环中,您处理&#34;流媒体&#34;访问模式。即你是连续地从/向内存加载/存储数据(并且缓存子系统对于大&#34; n&#34;值没有帮助。因此,根据目标硬件,多线程(SIMD顶上)等的使用情况,您的循环最终可能会成为内存带宽限制。一旦你成为内存带宽绑定,你可以使用循环阻塞,非临时存储,积极预取等技术。所有这些技术都值得单独的文章,虽然用于预取/ NT - 你可以在英特尔编译器中使用一些pragma来玩。

    4. 如果n很大,并且你已经准备好了内存带宽问题,你可以使用 #pragma omp parallel for simd 这样的东西,它将同时线程并行化并向量化循环。然而,这个功能的质量只在非常新的编译器版本AFAIK中变得不错,所以也许您更喜欢半手动分割n。即 n = n1xn2xn3 ,其中n1 - 是在线程之间分配的迭代次数,n2 - 用于缓存阻塞,n3 - 用于向量化。重写给定循环以使其成为3个嵌套循环的循环,其中外循环具有n1次迭代(并且应用了#pragma omp parallel),下一级循环具有n2次迭代,n3 - 是最内层的(其中应用了#pragma omp simd)。

    5. 包含语法示例和更多信息的一些最新链接

      注意1:我很抱歉我不提供各种代码段。在这里提供它们至少有两个合理的理由:1。我的5个子弹几乎适用于很多内核,而不仅仅适用于你的内核。 2.另一方面,编译指示/手动重写技术和相应的性能结果的具体组合将根据目标平台,ISA和编译器版本而变化。

      注2:关于GPU问题的最新评论。想想您的循环与简单的行业基准测试,如LINPACK或STREAM。事实上,你的循环最终可能会变得非常相似。现在想想用于LINPACK / STREAM的x86 CPU,特别是Intel Xeon Phi平台特性。它们确实非常好,并且通过高带宽内存平台(如Xeon Phi 2nd gen)将变得更好。所以理论上没有任何理由认为你的给定循环没有很好地映射到x86硬件的至少一些变体(请注意,我没有说任意的类似的东西宇宙中的内核)。

答案 2 :(得分:0)

我认为,n很大。您可以通过启动k个线程在k个CPU上分配工作负载,并为每个线程提供n/k个元素。为每个线程使用大块连续数据,不要进行细粒度交错。尝试将块与缓存行对齐。

如果您计划扩展到多个NUMA节点,请考虑明确将工作负载块复制到节点,运行该线程,然后复制结果。在这种情况下,它可能没有用,因为每个步骤的工作量非常简单。你必须为此运行测试。

答案 3 :(得分:0)

手动循环展开是优化代码的简单方法,以下是我的代码。原始loop的成本为618.48毫秒,而loop2的成本为381.10毫秒,编译器为GCC,选项为“-O2”。我没有Intel ICC验证代码,但我认为优化原则是相同的。

类似地,我做了一些实验,将两个程序的执行时间与XOR两个存储器块进行比较,一个程序在SIMD指令的帮助下进行矢量化,而另一个程序是手动循环展开的。如果您有兴趣,请参阅here

P.S。当然loop2仅在n为偶数时才有效。

#include <stdlib.h>
#include <stdio.h>
#include <sys/time.h>

#define LEN 512*1024
#define times  1000

void loop(int n, double* a, double const* b){
    int i;
    for(i = 0; i < n; ++i, ++a, ++b)
        *a *= *b;
}

void loop2(int n, double* a, double const* b){
    int i;
    for(i = 0; i < n; i=i+2, a=a+2, b=b+2)
        *a *= *b;
        *(a+1) *= *(b+1);
}


int main(void){
    double *la, *lb;
    struct timeval begin, end;
    int i;

    la = (double *)malloc(LEN*sizeof(double));
    lb = (double *)malloc(LEN*sizeof(double));
    gettimeofday(&begin, NULL);
    for(i = 0; i < times; ++i){
        loop(LEN, la, lb);
    }
    gettimeofday(&end, NULL);
    printf("Time cost : %.2f ms\n",(end.tv_sec-begin.tv_sec)*1000.0\
            +(end.tv_usec-begin.tv_usec)/1000.0);

    gettimeofday(&begin, NULL);
    for(i = 0; i < times; ++i){
        loop2(LEN, la, lb);
    }
    gettimeofday(&end, NULL);
    printf("Time cost : %.2f ms\n",(end.tv_sec-begin.tv_sec)*1000.0\
            +(end.tv_usec-begin.tv_usec)/1000.0);

    free(la);
    free(lb);
    return 0;
}