C++ 如何优化简单循环?

C++ 如何优化简单循环?,c++,optimization,vectorization,multicore,simd,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; } void循环(int n,double*a,double const*b) { #布拉格马伊夫代普 对于(int i=0;i

循环很简单

void loop(int n, double* a, double const* b)
{
#pragma ivdep
    for (int i = 0; i < n; ++i, ++a, ++b)
        *a *= *b;
}
void循环(int n,double*a,double const*b)
{
#布拉格马伊夫代普
对于(int i=0;i

我使用英特尔C++编译器,使用PrimaIVDEP进行优化。有什么方法可以让它表现得更好,比如同时使用多核和矢量化,或者其他技术?

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

在较旧的ICC版本中,“restrict”是向编译器提供密钥信息的唯一干净方法。在较新的版本中,有一些更干净的方法可以提供比
ivdep
更有力的保证(事实上
ivdep
对优化器的承诺比它看起来更弱,通常没有预期的效果)


但是如果
n
很大,那么整个过程都会被缓存未命中所控制,因此没有任何局部优化可以提供帮助。

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


如果计划扩展到一个以上的NUMA节点,请考虑将工作负载块显式复制到节点,线程继续运行,并复制结果。在这种情况下,它可能没有真正的帮助,因为每个步骤的工作负载都非常简单。您必须为此运行测试。

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

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

当然,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;
}
#包括
#包括
#包括
#定义LEN 512*1024
#定义乘以1000
无效循环(整数n,双*a,双常量*b){
int i;
对于(i=0;i
  • 编译器绝对可以对该循环进行矢量化。但要确保循环实际上是矢量化的(使用编译器'-qopt-report5,汇编输出等任何其他技术)。另一种方法是使用-no-vec选项创建性能基线(该选项将禁用ivdep驱动和自动矢量化),然后比较执行时间。这不是检查矢量化是否存在的好方法,但对于下一步的总体性能分析非常有用
  • 若循环还并没有被真正矢量化,那个么请确保将编译器推到自动矢量化。要推送编译器,请参阅下一个项目符号。请注意,即使循环成功地自动矢量化,下一个项目符号也可能有用

  • 要推动编译器对其进行矢量化,请使用:(a)restrict关键字来“消除”a和b指针的歧义(有人已经向您提出了建议)。(b) #pragma omp simd(它比ivdep更具可移植性和灵活性,但也有一个缺点,即在英特尔编译器第14版之前的旧编译器中不受支持,对于其他循环更“危险”)。再次强调:given bullet似乎可以做与ivdep相同的事情,但根据不同的情况,它可能是更好、更强大的选择

  • 给定的循环具有细粒度迭代(每个迭代的计算量太小),并且总体上不是纯粹的计算限制(因此CPU从缓存/内存加载/存储数据所花费的努力/周期与执行乘法所花费的努力/周期相比是可比的)。展开通常是稍微减轻这些缺点的好方法。但我建议使用#pragma unroll显式要求编译器展开它。事实上,对于某些编译器版本,展开将自动进行。同样,只要编译器使用-qopt-report5、循环汇编或英特尔(矢量化)进行检查,您都可以进行检查:

  • 在给定的循环中,处理“流”访问模式。也就是说,您正在连续地从内存加载/存储数据(对于大的“n”值,缓存子系统不会有多大帮助)。因此,根据目标硬件、多线程(SIMD之上)的使用情况等,您的循环最终可能会受到内存带宽的限制。一旦内存带宽受限,就可以使用循环阻塞、非时态存储、主动预取等技术。所有这些技术都值得单独发表,尽管对于预取/NT存储,您可以在英特尔编译器中使用一些pragma