以前做的TMS320 C6x DSPs,下面是一些优化上的建议
一、c6x的编译的常用选项
(一)c6x的编译程序为“cl6x.exe”使用的方法
Cl6x [options] [filenames]
Cl6x: 编译程序
Options:编译选项
Filenames:C或汇编源文件
说明:
编译选项是一个字母或者两个字母,对大小写不敏感。
编译选项的前面需要有一个“-”符号。
一个字母的选项可以合并在一起。比如“-sgq”与“-s -g -q”相同。
两个字母的选项如果第一个字母相同也可以合并在一起。比如“-mgt”与“-mg -mt”相同。
(二)有关优化的选项
-mt:表示在程序中没有使用alaising技术,这使得编译器可以进行比较好的优化。
-o3:对文件级别进行最强的优化,一般在编译时应该使用这个选项。但是在个别情况下使用这个选项优化程序可能会出现错误(-o2有相同现象,-o0和-o1不会出现错误)。可能是在优化循环,组织流水线的时候发生错误。如果有这种现象出现可以同时使用-g选项,程序优化就不会出现错误,但是优化效果会下降。另外可以调整程序的表达方式,可能会避免编译器发生错误。
-pm:在程序级别进行优化。可以将所以文件联合在一起进行优化,主要有去掉没有被调用的函数、总是常数的变量以及没有使用的函数返回值。建议由程序员自己进行这种优化工作。使用这个选项在win98下编译可能会出现找不到编译程序的情况。
-ms0:不使用冗余循环进行优化,减小程序的大小。一般情况下这个选项对程序大小的优化作用不明显。
-mh[n]:去掉流水线的epilog,减小程序的大小。这个选项的作用比较明显。但是有可能出现读取地址超出有效范围的问题,所以要在数据段的开始和结尾处增加一些pading,或者在分配内存时保证数组的前面和后面一段范围内都是有效的地址。可选的参数n给出这种pading的长度字节数。
(三)保留编译和优化信息的选项
-k:保留优化后生成汇编语言文件。
-s:汇编语言文件中加入优化信息,如果没有则加入C语言源程序作为注释。
-mw:在汇编语言文件加入软件流水线信息。
(四)有关调试和剖析的选项
-g:允许符号调试,在“out”文件中包含符号信息和行号信息,可以在c语言级别进行调试和剖析。使用联合使用-g、-mt和-o3可以保证能够进行符号调试的情况下最大限度的优化。
-mg:允许profile优化后的程序。 在“out”文件中包含符号信息和很少的行号信息。允许在c语言的函数基本进行剖析。
如果联合使用这两个选项,-g选项可能被忽略,结果与只用-mg相同。
(五)其它类型
-mln:生成大内存模式的程序。
-ml0:缺省情况下将集合变量(数组和结构)作为far型。
-ml1:缺省情况下将全部函数作为far型
-ml2: 等于-ml0加-ml1
-ml3: 缺省情况下将全部数据和函数作为far型
(六)建议使用的编译方式
Cl6x -gk -mt -o3 -mw -ss“filename”
方式1用于程序的调试,这种方式具有比较强的优化能力,并且支持符号调试。在编译的过程中不会发生错误。由于生成的“out”文件中包含了符号信息和行号信息,所以比较大。
Cl6x -k -mgt -o3 -mw -ss“filename”
方式2用于程序的剖析(profile),这种方式的优化能力几乎最强(绝大多数情况下与方式3相同),并且支持对程序进行profile。文件中只包含了符号信息和很少的行号信息,所以“out”文件比较小。
Cl6x -k -mt -o3 -mw -ss“filename”
方式3用于最终的发行版本程序,可以对程序进行最强的优化,并且去掉了全部的符号和行号信息,所以“out”文件比较小。
由多个文件组成的程序应该编写makefile,将编译参数放在该文件中,并在其中说明使用的编译器的版本号。
(七)连接参数
-heap:指定堆的大小
-stack: 指定栈的大小
连接的各种选项应该统一放在“cmd”文件中
二、双重循环和多重循环的优化总结
双重循环多重循环看起来比较复杂,但实际上多重循环优化方法比较简单,就在于一个字:“拆”,一旦完成这一步之后,多重循环就成为单层循环,优化就可以按照普通的单层循环来做了。
多重循环的特点是在优化器优化时只在最内层循环中形成一个pipeline,这样循环语句就不能充分利用C6的软件流水线,而且对于内部循环的次数较少的情况,消耗在prolog和eplog上的cycle数也是不可忽视的。针对这种状况可以考虑将多重循环拆开形成一个单层循环,可以拆外层循环也可以拆内层循环,一般视具体情况而定。这样就可以充分利用优化器构成的Pipeline。如下例:
void fir2(const short input[], const short coefs[], short out[])
{
int i, j;
int sum = 0;
for (i = 0; i < 40; i++)
{
for (j = 0; j < 16; j++)
sum += coefs[j] * input[i + 15 - j];
out[i] = (sum >> 15);
}
内层循环循环次数较少,运算量也不大,资源方面只占用了一个乘法器,一个cycle只使用一次乘法器,而事实上我们可以在一个cycle内使用两个乘法器,所以还可以充分利用另外的一个乘法器。因此考虑将内层循环拆开来执行,如下:
void fir2_u(const short input[], const short coefs[], short out[])
{
int i, j;
int sum;
for (i = 0; i < 40; i++)
{
sum = coefs[0] * input[i + 15];
sum += coefs[1] * input[i + 14];
sum += coefs[2] * input[i + 13];
sum += coefs[3] * input[i + 12];
sum += coefs[4] * input[i + 11];
sum += coefs[5] * input[i + 10];
sum += coefs[6] * input[i + 9];
sum += coefs[7] * input[i + 8];
sum += coefs[8] * input[i + 7];
sum += coefs[9] * input[i + 6];
sum += coefs[10] * input[i + 5];
sum += coefs[11] * input[i + 4];
sum += coefs[12] * input[i + 3];
sum += coefs[13] * input[i + 2];
sum += coefs[14] * input[i + 1];
sum += coefs[15] * input[i + 0];
out[i] = (sum >> 15);
}
这样虽然代码长度增加了,可变成了单循环,所有的运算都参加到pipeline中来,在Piped loop kernal 中产生每一个cycle内都使用了两个乘法器,充分利用了DSP内部的资源,提高了运行效率。又如下例:
tot = 4;
for (k = 0; k < 4; k++)
{
max = 0;
for (i = k; i < 44; i += STEP)
{
s = 0;
for (j = i; j < 44; j++)
s = L_mac(s, x[j], h[j - i]);
y32[i] = s;
s = L_abs(s);
if (L_sub(s, max) > (Word32) 0)
max = s;
}
tot = L_add(tot, L_shr(max, 1));
}
在这个多层循环中一共有三层循环,而最内层的循环的运算量很小,只有一次乘累加操作,而我们知道C6中一个packet中可以做两个乘累加运算,所以为了增加内部循环的运算,减少外部循环的层数,我们可以将第一层循环的操作拆开,其负责的运算加入到内部循环中,也就是在内层循环中一次做四次的乘累加运算,这样将多次操作形成pipeline,提高了运行效率,优化后的C代码如下:
tot = 4;
max0=0;
max1=0;
max2=0;
max3=0;
for (i = 0; i <44; i += STEP) //STEP=4, 11 times cirs
{
//code
for (j=0;j<=40-i;j++)
{s0=(Word32)(_sadd(s0,_smpy(hh[j],xx[j+i])));
s1=(Word32)(_sadd(s1,_smpy(hh[j],xx[j+i+1])));
s2=(Word32)(_sadd(s2,_smpy(hh[j],xx[j+i+2])));
s3=(Word32)(_sadd(s3,_smpy(hh[j],xx[j+i+3])));
}
}
//code
CCS的优化:
三、16位变为32位操作,使用intrinsic函数,用const等。
1、源代码:
Word32 L_mpy_ll(Word32 L_var1, Word32 L_var2)
{
double aReg;
Word32 lvar;
/* (unsigned)low1 * (unsigned)low1 */
aReg = (double)(0xffff & L_var1) * (double)(0xffff & L_var2) * 2.0;
/* >> 16 */
aReg = (aReg / 65536);
aReg = floor(aReg);
/* (unsigned)low1 * (signed)high2 */
aReg += (double)(0xffff & L_var1) * ((double)L_shr(L_var2,16)) * 2.0;
/* (unsigned)low2 * (signed)high1 */
aReg += (double)(0xffff & L_var2) * ((double)L_shr(L_var1,16)) * 2.0;
/* >> 16 */
aReg = (aReg / 65536);
aReg = floor(aReg);
/* (signed)high1 * (signed)high2 */
aReg += (double)(L_shr(L_var1,16)) * (double)(L_shr(L_var2,16)) * 2.0;
/* saturate result.. */
lvar = L_saturate(aReg);
return(lvar);
}
2、改编后的代码:
static inline Word32 L_mpy_ll(Word32 L_var1, Word32 L_var2)
{
Word32 aReg_hh;
Word40 aReg,aReg_ll,aReg_lh,aReg_hl;
aReg_ll = (Word40)_mpyu(L_var1, L_var2)>>16;
aReg_lh = (Word40)_mpyluhs(L_var1, L_var2);
aReg_hl = (Word40)_mpyhslu(L_var1, L_var2);
aReg_hh = _smpyh(L_var1, L_var2);
aReg = _lsadd(aReg_ll, _lsadd(aReg_lh, aReg_hl));
aReg = _lsadd(aReg>>15, aReg_hh);
return(_sat(aReg));
}
3、优化方法说明:
C6000编译器提供的intrinsic 可快速优化C代码,intrinsic用前下划线表示同调用函数一样可以调用它,即直接内联为C6000的函数。例如,在上例的源代码中没有使用intrinsics,每一行C代码需多个指令周期,在改编后的代码中,每一行代码仅需一个指令周期。例如,
“aReg_ll = (Word40)_mpyu(L_var1, L_var2)>>16”中“_mpyu”就是一个intrinsics函数,它表示两个无符号数的高16位相乘,结果返回。C6000支持的所有intrinsics指令及其功能参见《TMS320C6000系列DSP的原理与应用》一书的第265、266页,该书还提供了另外的例子。这些内联函数定义在CCS所在的C6000/CGTOOLS/Include目录下的C6X.h文件中。下面这个例子是C6000的“Programmer's Guide”上提取的使用intrinsics优化C代码的例子。
源代码:
int dotprod(const short *a, const short *b, unsigned int N)
{
int i, sum = 0;
for (i = 0; i < N; i++)
sum += a[i] * b[i];
return sum;
}
改编后代码:
int dotprod(const int *a, const int *b, unsigned int N)
{
int i, sum1 = 0, sum2 = 0;
for (i = 0; i < (N >> 1); i++)
{
sum1 += _mpy (a[i], b[i]);
sum2 += _mpyh(a[i], b[i]);
}
return sum1 + sum2;
}
技巧:
在C语言的调试全部通过以后,可以尝试将尽可能多的语句使用intrinsics函数加以改编,尤其在循环体内,这种改编可以大幅度减少执行时间。
四、
1、源代码:
void fir_fxd1(short input[], short coefs[], short out[])
{
int i, j;
for (i = 0; i < 40; i++)
{
for (j = 0; j < 16; j++)
out[i*16+j]= coefs[j] * input[i + 15 - j];
}
}
2、改编后的代码:
void fir_fxd2(const short input[], const short coefs[], short out[])
{
int i, j;
for (i = 0; i < 40; i++)
{
for (j = 0; j < 16; j++)
out[i*16+j]= coefs[j] * input[i + 15 - j];
}
3、优化方法说明:
C6000编译器如果确定两条指令是不相关的,则安排它们并行执行。 关键字const可以指定一个变量或者一个变量的存储单元保持不变。这有助于帮助编译器确定指令的不相关性。例如上例中,源代码不能并行执行,而结果改编后的代码可以并行执行。
4、技巧:
使用const可以限定目标,确定存在于循环迭代中的存储器的不相关性。
五、
1、源代码:
void vecsum(short *sum, short *in1, short *in2, unsigned int N)
{
int i;
for (i = 0; i < N; i++)
sum[i] = in1[i] + in2[i];
}
2、改编后的代码:
void vecsum6(int *sum, const int *in1, const int *in2, unsigned int N)
{
int i;
int sz = N >> 2;
_nassert(N >= 20);
for (i = 0; i < sz; i += 2)
{
sum[i] = _add2(in1[i] , in2[i]);
sum[i+1] = _add2(in1[i+1], in2[i+1]);
}
}
3、优化方法说明:
源代码中,函数变量的定义是 short *sum, short *in1, short *in2, 改编后的代码函数变量是 int *sum, const int *in1, const int *in2, 整数类型由16位改编成32位,这时使用内联指令“_add2”一次可以完成两组16位整数的加法, 效率提高一倍。注意这里还使用了关键字const和内联指令_nassert优化源代码。
4、技巧:
用内联指令_add2、_mpyhl、_mpylh完成两组16位数的加法和乘法,效率比单纯16位数的加法和乘法提高一倍。