提高正弦/余弦和大型阵列的计算速度

时间:2016-02-19 19:39:03

标签: c++ c visual-c++ visual-c++-2010

用于信号处理我需要计算相对较大的C数组,如下面的代码部分所示。到目前为止,这工作正常,遗憾的是,实施缓慢。 " calibdata"的大小是150k,需要针对不同的频率/相位进行计算。有没有办法显着提高速度?在MATLAB中对逻辑索引进行相同的操作要快得多。

我已经尝试过了:

  • 使用正则的泰勒近似:没有显着改善。
  • 使用std :: vector,也没有太大的改进。

代码:

double phase_func(double* calibdata, long size, double* freqscale, double fs, double phase, int currentcarrier){
for (int i = 0; i < size; i++)
    result += calibdata[i] * cos((2 * PI*freqscale[currentcarrier] * i / fs) + (phase*(PI / 180) - (PI / 2)));

result = fabs(result / size);

return result;}

祝你好运, 托马斯

6 个答案:

答案 0 :(得分:4)

优化代码以提高速度时,步骤1是启用编译器优化。我希望你已经做到了。

第2步是分析代码并确切了解时间的使用情况。如果没有分析,你只是在猜测,你最终可能会尝试优化错误的东西。

例如,您的猜测似乎是cos函数是瓶颈。但另一种可能性是角度的计算是瓶颈。这是我如何重构代码以减少计算角度所花费的时间。

double phase_func(double* calibdata, long size, double* freqscale, double fs, double phase, int currentcarrier)
{
    double result = 0;
    double angle = phase * (PI / 180) - (PI / 2);
    double delta = 2 * PI * freqscale[currentcarrier] / fs;
    for (int i = 0; i < size; i++)
    {
        result += calibdata[i] * cos( angle );
        angle += delta;
    }
    return fabs(result / size);
}

答案 1 :(得分:3)

好吧,我可能会因为这个问题而被鞭打,但我会使用GPU来实现这个目标。因为你的数组看起来不是自我引用的,所以你为大型数组获得的最佳加速是通过并行化...到目前为止。我没有使用MATLAB,但我只是在MathWorks网站上快速搜索GPU利用率:

http://www.mathworks.com/company/newsletters/articles/gpu-programming-in-matlab.html?requestedDomain=www.mathworks.com

在MATLAB之外,您可以自己使用OpenCL或CUDA。

答案 2 :(得分:1)

你执行时间的敌人是:

  • 函数调用(包括循环中的隐式函数)
  • 从不同区域访问数据
  • 操作不同的说明

您应该研究数据驱动编程并有效地使用数据缓存。

无论是硬件支持还是软件支持部门,其性质都需要很长时间。如果可能的话,通过更改数字基数或分解出循环(如果可能)来消除。

函数调用

最有效的执行方法是顺序执行。处理器针对此进行了优化。分支可能要求处理器执行一些额外的计算(分支预测)或重新加载指令高速缓存/流水线。浪费时间(可能花在执行数据指令上)。

对此的优化是使用循环展开和内联小函数等技术。还可以通过简化表达式和使用布尔代数来减少分支数量。

访问不同区域的数据 现代处理器经过优化,可以对本地数据(一个区域内的数据)进行操作。一个例子是使用数据加载内部缓存。具体来说,使用数据加载缓存行。例如,如果数组中的数据位于一个位置,而余弦数据位于另一个位置,则可能导致数据缓存重新加载,再次浪费时间。

更好的解决方案是连续放置所有数据或连续访问所有数据。不是对余弦表进行许多不连续的访问,而是按顺序查找一批余弦值(不需要任何其他数据访问)。

不同的说明

现代处理器在处理一批类似指令方面更有效。例如,模式加载,添加,存储对于块执行所有加载时更有效,然后全部添加,然后全部存储。

摘要

以下是一个例子:

register double result = 0.0;
register unsigned int i = 0U;
for (i = 0; i < size; i += 2)
{
    register double cos_angle1 = /* ... */;
    register double cos_angle2 = /* ... */;
    result += calibdata[i + 0] * cos_angle1;
    result += calibdata[i + 1] * cos_angle2;
}

上述循环展开,并且操作以组的形式执行 尽管可能不推荐使用关键字register,但建议编译器使用专用寄存器(如果可能)。

答案 3 :(得分:0)

您可以尝试使用基于复指数的余弦定义:

其中j^2=-1

存储exp((2 * PI*freqscale[currentcarrier] / fs)*j)exp(phase*j)。评估cos(...)然后恢复for循环中的几个产品和添加内容,而sin()cos()exp()只会被调用几次。

以下是实施:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <complex.h>
#include <time.h> 

#define PI   3.141592653589

typedef struct cos_plan{
    double complex* expo;
    int size;
}cos_plan;

double phase_func(double* calibdata, long size, double* freqscale, double fs, double phase, int currentcarrier){
    double result=0;  //initialization
    for (int i = 0; i < size; i++){

        result += calibdata[i] * cos ( (2 * PI*freqscale[currentcarrier] * i / fs) + (phase*(PI / 180.) - (PI / 2.)) );

        //printf("i %d cos %g\n",i,cos ( (2 * PI*freqscale[currentcarrier] * i / fs) + (phase*(PI / 180.) - (PI / 2.)) ));
    }
    result = fabs(result / size);

    return result;
}

double phase_func2(double* calibdata, long size, double* freqscale, double fs, double phase, int currentcarrier, cos_plan* plan){

    //first, let's compute the exponentials:
    //double complex phaseexp=cos(phase*(PI / 180.) - (PI / 2.))+sin(phase*(PI / 180.) - (PI / 2.))*I;
    //double complex phaseexpm=conj(phaseexp);

    double phasesin=sin(phase*(PI / 180.) - (PI / 2.));
    double phasecos=cos(phase*(PI / 180.) - (PI / 2.));

    if (plan->size<size){
        double complex *tmp=realloc(plan->expo,size*sizeof(double complex));
        if(tmp==NULL){fprintf(stderr,"realloc failed\n");exit(1);}
        plan->expo=tmp;
        plan->size=size;
    }

    plan->expo[0]=1;
    //plan->expo[1]=exp(2 *I* PI*freqscale[currentcarrier]/fs);
    plan->expo[1]=cos(2 * PI*freqscale[currentcarrier]/fs)+sin(2 * PI*freqscale[currentcarrier]/fs)*I;
    //printf("%g %g\n",creall(plan->expo[1]),cimagl(plan->expo[1]));
    for(int i=2;i<size;i++){
        if(i%2==0){
            plan->expo[i]=plan->expo[i/2]*plan->expo[i/2];
        }else{
            plan->expo[i]=plan->expo[i/2]*plan->expo[i/2+1];
        }
    }
    //computing the result
    double result=0;  //initialization
    for(int i=0;i<size;i++){
        //double coss=0.5*creall(plan->expo[i]*phaseexp+conj(plan->expo[i])*phaseexpm);
        double coss=creall(plan->expo[i])*phasecos-cimagl(plan->expo[i])*phasesin;
        //printf("i %d cos %g\n",i,coss);
        result+=calibdata[i] *coss;
    }

    result = fabs(result / size);

    return result;
}

int main(){
    //the parameters

    long n=100000000;
    double* calibdata=malloc(n*sizeof(double));
    if(calibdata==NULL){fprintf(stderr,"malloc failed\n");exit(1);}

    int freqnb=42;
    double* freqscale=malloc(freqnb*sizeof(double));
    if(freqscale==NULL){fprintf(stderr,"malloc failed\n");exit(1);}
    for (int i = 0; i < freqnb; i++){
        freqscale[i]=i*i*0.007+i;
    }

    double fs=n;

    double phase=0.05;

    //populate calibdata
    for (int i = 0; i < n; i++){
        calibdata[i]=i/((double)n);
        calibdata[i]=calibdata[i]*calibdata[i]-calibdata[i]+0.007/(calibdata[i]+3.0);
    }

    //call to sample code
    clock_t t;
    t = clock();
    double res=phase_func(calibdata,n, freqscale, fs, phase, 13);
    t = clock() - t;

    printf("first call got %g in %g seconds.\n",res,((float)t)/CLOCKS_PER_SEC);


    //initialize
    cos_plan plan;
    plan.expo=malloc(n*sizeof(double complex));
    plan.size=n;

    t = clock();
    res=phase_func2(calibdata,n, freqscale, fs, phase, 13,&plan);
    t = clock() - t;

    printf("second call got %g in %g seconds.\n",res,((float)t)/CLOCKS_PER_SEC);




    //cleaning

    free(plan.expo);

    free(calibdata);
    free(freqscale);

    return 0;
}

gcc main.c -o main -std=c99 -lm -Wall -O3汇编。使用您提供的代码,我的计算机上的size=100000000 需要 8秒,而建议的解决方案的执行时间需要1.5秒 ...它不是如此令人印象深刻,但这并不是可以忽略不计的。

所呈现的解决方案不涉及在for循环中对cos sin的任何调用。实际上,只有乘法和加法。瓶颈是内存带宽或测试以及通过平方对指数内存的访问(很可能是第一个问题,因为我添加使用额外的复数数组)。

对于c中的复数,请参阅:

如果问题是内存带宽,则需要并行性......直接计算cos会更容易。如果freqscale[currentcarrier] / fs是整数,则可以执行额外的简化。你的问题非常接近Discrete Cosine Transform的计算,目前的技巧接近于离散傅立叶变换,而FFTW库非常擅长计算这些变换。

请注意,由于失去重要性,当前代码可能会产生真空结果:resultcos(...)*calibdata[]较大时可能比size大得多。使用部分总和可以解决问题。

答案 4 :(得分:0)

  1. 简单的trig标识可以消除- (PI / 2)。这也比尝试使用machine_PI的减法更准确。当值接近π/ 2时,这很重要。

    cosine(x - π/2) == -sine(x)
    
  2. 使用constrestrict:优秀的编译器可以利用这些知识执行更多优化。 (另见@user3528438

    // double phase_func(double* calibdata, long size, 
    //     double* freqscale, double fs, double phase, int currentcarrier) {
    double phase_func(const double* restrict calibdata, long size, 
        const double* restrict freqscale, double fs, double phase, int currentcarrier) {
    
  3. 某些平台使用floatdouble执行更快的计算,并且可以容忍精度损失。因人而异。两种方式的配置文件代码。

    // result += calibdata[i] * cos(...
    result += calibdata[i] * cosf(...
    
  4. 尽量减少重新计算。

    double angle_delta = ...;
    double angle_current = ...;
    for (int i = 0; i < size; i++) {
      result += calibdata[i] * cos(angle_current);
      angle_current += angle_delta;
    }
    
  5. 不清楚代码使用long sizeint currentcarrier的原因。我希望使用相同的类型并使用类型size_t。这是数组索引的惯用语。 @Daniel Jour

  6. 反转循环可以允许比较为0而不是与变量进行比较。有时可以获得适度的性能提升。

  7. 确保编译器优化得到很好的启用。

  8. 一起

    double phase_func2(const double* restrict calibdata, size_t size,
        const double* restrict freqscale, double fs, double phase,
        size_t currentcarrier) {
    
      double result = 0.0;
      double angle_delta = 2.0 * PI * freqscale[currentcarrier] / fs;
      double angle_current = angle_delta * (size - 1) + phase * (PI / 180);
      size_t i = size;
      while (i) {
        result -= calibdata[--i] * sinf(angle_current);
        angle_current -= angle_delta;
      }
      result = fabs(result / size);
      return result;
    }
    

答案 5 :(得分:0)

利用您拥有的核心,而无需使用GPU,使用OpenMP。使用VS2015进行测试时,优化程序将不变量提升出循环。启用AVX2和OpenMP。

double phase_func3(double* calibdata, const int size, const double* freqscale, 
    const double fs, const double phase, const size_t currentcarrier)
{
    double result{};
    constexpr double PI = 3.141592653589;

#pragma omp parallel
#pragma omp for reduction(+: result)
    for (int i = 0; i < size; ++i) {
        result += calibdata[i] *
            cos( (2 * PI*freqscale[currentcarrier] * i / fs) + (phase*(PI / 180.0) - (PI / 2.0)));
    }
    result = fabs(result / size);
    return result;
}

启用AVX的原始版本采用: ~1.4秒
并添加OpenMP将其降低到: ~0.51秒

两个pragma和一个编译器开关的相当不错的回报。