用于信号处理我需要计算相对较大的C数组,如下面的代码部分所示。到目前为止,这工作正常,遗憾的是,实施缓慢。 " calibdata"的大小是150k,需要针对不同的频率/相位进行计算。有没有办法显着提高速度?在MATLAB中对逻辑索引进行相同的操作要快得多。
我已经尝试过了:
代码:
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;}
祝你好运, 托马斯
答案 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利用率:
在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库非常擅长计算这些变换。
请注意,由于失去重要性,当前代码可能会产生真空结果:result
在cos(...)*calibdata[]
较大时可能比size
大得多。使用部分总和可以解决问题。
答案 4 :(得分:0)
简单的trig标识可以消除- (PI / 2)
。这也比尝试使用machine_PI
的减法更准确。当值接近π/ 2时,这很重要。
cosine(x - π/2) == -sine(x)
使用const
和restrict
:优秀的编译器可以利用这些知识执行更多优化。 (另见@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) {
某些平台使用float
与double
执行更快的计算,并且可以容忍精度损失。因人而异。两种方式的配置文件代码。
// result += calibdata[i] * cos(...
result += calibdata[i] * cosf(...
尽量减少重新计算。
double angle_delta = ...;
double angle_current = ...;
for (int i = 0; i < size; i++) {
result += calibdata[i] * cos(angle_current);
angle_current += angle_delta;
}
不清楚代码使用long size
和int currentcarrier
的原因。我希望使用相同的类型并使用类型size_t
。这是数组索引的惯用语。 @Daniel Jour
反转循环可以允许比较为0而不是与变量进行比较。有时可以获得适度的性能提升。
确保编译器优化得到很好的启用。
一起
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和一个编译器开关的相当不错的回报。