在CPU上开发SDR程序,直接写C/C++代码常常不能满足程序的实时性要求,因此需要对代码进行优化。优化前首先要测量程序各模块的运行时间,找出瓶颈所在,然后逐个优化。最近优化了几个简单的模块,记下来以备将来参考。

为什么不直接用汇编语言进行优化呢?因为我们尝试之后发现,要赛过编译器是很难的。如果我们直接写汇编代码,CPU就会直接按照汇编指令的顺序执行;而如果用C/C++代码和intrinsic函数写代码,GCC还会对程序进行优化。GCC凝聚了多少程序员的智慧呀,简直太聪明了,俺们这种小码农是比不过的。

有哪些intrinsic函数可用呢,这得去查Intel的手册。比如说我用到了:
“64-ia-32-architectures-software-developer-instruction-set-reference-manual”
“64-ia-32-architectures-optimization-manual”
我觉得寻找合适的函数这是一个比较麻烦的过程。

优化的方法就是把程序改成向量运算。典型的适合改成向量运算的模块包括:滤波器,相关,积分,点积,还有很多跟矩阵相关的运算。因为CPU的SSE寄存器宽度很有限,根据处理器的不同可能有128bit或者256bit。以128bit为例,它能够同时处理4个float型变量。

下面是一个滑动相关的例子,经常会用在同步信号检测这种场合。

函数的功能:

1
2
corr_output[i] =
sum(input[i:i+integral_len].*conj(input[i+corr_distance:i+corr_distance+integral_len])

C代码是这样的:

1
2
3
4
5
6
7
corr_output[ii][0] = 0;
corr_output[ii][1] = 0;
for (int iCORR=0; iCORR<integral_len; iCORR++)
{
corr_output[ii][0] += input_i[iCORR][0]*input_d[iCORR][0] + input_i[iCORR][1]*input_d[iCORR][1];
corr_output[ii][1] += -input_i[iCORR][0]*input_d[iCORR][1] + input_i[iCORR][1]*input_d[iCORR][0];
}

Intrinsic代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int nloop = integral_len/2; // process two samples in each loop: x1*conj(y1) + x2*conj(y2)
__m128 product_real,product_imag,tmp_y;
__m128 sum_real = _mm_set_ps1(0.0f);
__m128 sum_imag = _mm_set_ps1(0.0f);
__m128 x;
__m128 y;

for (int i=0; i < nloop; i++)
{
x = _mm_loadu_ps(input_i); // load data from memory to register: x4 x3 x2 x1
y = _mm_loadu_ps(input_d); // load data from memory to register: y4 y3 y2 y1
product_real = _mm_mul_ps(x, y); // x4*y4, x3*y3, x2*y2, x1*y1
sum_real = _mm_add_ps(sum_real, product_real); // Add results into sum register
tmp_y = _mm_shuffle_ps(y,y, 0xb1); // [y4 y3 y2 y1] => [y3 y4 y1 y2]
product_imag = _mm_mul_ps(x,tmp_y); // x4*y3, x3*y4, x2*y1, x1*y2
sum_imag = _mm_addsub_ps(sum_imag, product_imag); // add resutls into sum register: x4*y3, -x3*y4, x2*y1, -x1*y2
input_i+=2; // shift pointer 2*4 bytes
input_d+=2;
}

sum_real = _mm_hadd_ps(sum_real,sum_real); // x3*y3+x4*y4, x1*y1+x2*y2
sum_real = _mm_hadd_ps(sum_real,sum_real); // x1*y1+x2*y2+x3*y3+x4*y4
_mm_store_ss(&(corr_output[ii][0]),sum_real); // store the real part into memory
sum_imag = _mm_hadd_ps(sum_imag,sum_imag); // -x3*y4+x4*y3, -x1*y2+x2*y1
sum_imag = _mm_hadd_ps(sum_imag,sum_imag); // -x1*y2+x2*y1-x3*y4+x4*y3
_mm_store_ss(&(corr_output[ii][1]),sum_imag); // store the imag part into memory


// the last sample if there is

if (integral_len%2==1)
{
// pointers are already moved to the last sample
corr_output[ii][0] += input_i[0][0]*input_d[0][0] + input_i[0][1]*input_d[0][1];
corr_output[ii][1] += -input_i[0][0]*input_d[0][1] + input_i[0][1]*input_d[0][0];
}

为什么这样写,在代码注释当中都有说明。简单的说一下思路就是:把两个长度为integral_len的复数向量,按照每两个复数一组,一共integral_len/2组,来做复数乘法,并累加求和。

运行时间对比:
corr_output[ii]的外面还有一层ii的循环,我们测量的是计算整个corr_output序列的的时间。
C代码的运行时间为:16.185ms
Intrinsic代码的运行时间为:3.548ms
时间缩短为原来的21.9%。
这个优化的版本肯定不是最优的,我相信高手一定能写出更好的代码。我只是简单的从手册中找到了可用的intrinsic函数,也许其他函数会更快。