C语言编程优化

 
开发执行在SoC内的嵌入式处理器核心程序时,通常有两个主要目的,即让处理器执行频率降到最低;以及使内存开销降到最小。这两项因素的重要性会因不同的计划而异,而以下两项关键将大幅影响设计团队满足这些目标的能力,即开发原始程序的编译器以最佳化程序代码的效率;以及用于开发原始程序代码的编程风格。本文将深入讨论这两种因素,并提出一些制作小型且快速之C程序的建议。
编译器通常由前端和后端两部份组成。前端通常是指语法和语义的处理过程,后端通常是指最佳化、程序代码产生,以及针对特定处理器的最佳化过程。很多好的编译器后端依赖于多层的中间表述(IR)。最佳化和程序代码产生从高层(类别输入程序的句法)到底层逐级地传递中间表述。与处理器无关的最佳化一般倾向于在编译过程早期于较高IR层上实现,而针对特定处理器的最佳化一般倾向于在编译过程的后期在底层IR上来实现。信息透过不同IR层向下传递,这样底层最佳化可以充分利用编译器早期处理得到的高层信息。
Tensilica 针对其Xtensa可配置处理器和Diamond标准处理器的XCC/C++编译器包含四个基本的最佳化级,从-O0到-O3,对应着不断提高的最佳化等级。表1描述了这些等级及其相对应的程序代码大小和内部过程分析(IPA)。通常情况下,XCC编译器一次最佳化一个文件,但是它也可以执行内部过程分析(透过加入IPA的编译选项)。当在多个原文件上最佳化整个应用程序时,最佳化将会被延迟到链接的步骤之后进行。表2描述了目前编译器(包括XCC编译器)支持的最佳化内容部份列表。
XCC 编译器还可以利用编译产生的性能分析数据。性能分析的反馈可以帮助编译器减轻分支跳转的延迟。另外,反馈可以让编译器只是插入那些最常用的函数(inline),并妥善处理常用程序代码段中缓存器溢出的问题。因此,性能分析反馈允许XCC编译器在所有地方进行正常最佳化的同时,还可以透过最佳化应用中的临界部份进行加速。
C 语言编码建议规则
为利用编译器获得最佳性能,程序设计师必须像编译器一样思考问题,并了解C语言和目标处理器之间的关系。以下一些基本原则可协助所有嵌入式程序设计师在不需很大努力的情况下获得性能更好的编译程序代码。
1. 观察编译后的程序代码
完全了解编译器对全部程序代码如何编译是不可能的。如果XCC编译器设置了─S或者-save-temps编译选项,编译将产生汇编输出,同时还有一些为了加强了解而添加的注释。对于那些性能要求很高的程序代码,你可以观察编译结果是否符合你的期望。如果不是,请考虑以下规则。
2. 了解混淆发生的情况
C 语言允许任意使用指针,这增加了混淆出现的机会,这允许程序用很多种方法去引用同一数据对象。如果全局变量的地址被作为子程序的参数传递,这个变量可以透过它的名字或透过指针被引用。这就是一种混淆,编译器必须保守地把这样的数据对象保存在内存中而不是缓存器中,并仔细地保持程序代码中可能引起混淆的变量的存取顺序。可考虑下面的程序代码:
void foo(int *a, int *b)
{
     int i;
          for (i=0; i<100; i++) {
          *a += b[i];
          }
}
您会设想编译器应该产生程序代码并在循环开始前将*a保存到一个缓存器中,同时在循环中把b[i]保存到一个缓存器里面然后将它加到*a所在的缓存器里。但事实上却是,编译器产生的结果是*a被放置在内存里面,因为a和b可以产生混淆情况,*a也许是b数组的一个元素。虽然看起来在这个例子中不太可能出现这种混淆,但是编译器是没法确定这种情况是否会发生的。有几个技巧可以针对混淆情况协助编译器实现更好的编译工作:你可以使用IPA编译选项进行编译,你可以用全局变量代替参数,你可以使用特殊编译选项进行编译,或在声明变量中使用_restrict属性。
3. 指针常常引起混淆
编译器识别指针指向的目标对象经常会遇到问题。程序设计师可透过使用本地变量帮助编译器避免混淆,具体方法是使用本地变量储存依据指针存取获得的值,因为不直接的作业和调用会影响指针引用的值而非本地变量的值。因此,编译器会把本地变量放到缓存器中。
以下例子显示如何正确使用指针以避免混淆并产生更好的编译程序代码。在这个例子中,最佳化者不知道*p++=0是否会修改len,所以它不能把len放到缓存器内获得性能提升。相反地,在每个循环中,len都被放到了内存内。
int len = 10;
void
zero(char *p)
{
     int i;
     for (i=0; i
}
透过使用本地变量而非全局变量,可以避免混淆。
int len = 10;
void
zero(char *p)
{
     int local_len = len;
     int i;
     for (i=0; i< local_len; i++) *p++ = 0;
}
4. const 和restrict限定词
_restrict 限定词告诉编译器可以假设有资格的指针是唯一存取某内存或数据对象的方式。透过这个指针的Load和Store作业,将不会引起与这个函数内部其它Load和Store作业的混淆,除非透过这个指针存取。例如:
float x[ARRAY_SIZE];
float *c = x;
void f4_opt(int n, float * __restrict a, float * __restrict b)
{
     int i;
     /* No data dependence across iterations because of __restrict */
     for (i = 0; i < n; i++)
     a[i] = b[i] + c[i];
}
5. 使用本地变量
这是因为全局变量会在整个程序的生命周期里面保留数值。编译器必须认为全局变量可能透过指针被存取。可考虑下列程序代码:
int g;
void foo()
{
     int i;
     for (i=0; i<100; i++){
         fred(i,g);
     }
}
理想情况下,g在每次fred循环时被加载一次,且其值将被传递到一个缓存器内给fred函数使用。但编译器不知道fred是否会修改g的值。如果fred不会修改g的值,你应该像下面一样使用本地变量。这样做可以避免每次调用fred函数时加载g到一个缓存器里面。
int g;
void foo()
{
     int i, local_g=g;
     for (i=0; i<100; i++){
         fred(i,local_g);
     }
}
6. 使用正确的数据类型
C 程序设计师对于数据类型一般都会有他们习惯上的假设,但是编译器却需要很谨慎地对待这些假设。例如,在几乎所有现代的计算机架构上,一个unsigned char使用8位表示从0到255。一个C程序会假设对值为255的unsigned char加1会使其变为0。而实际上,现代32位处理器不会执行上述的8位加法,而是进行32位数值加法。因此,如果一个unsigned char的本地变量进行加法,编译器必须使用多条指令进行运算以保证加法后的符号扩展。因此,针对各种变量尤其是循环索引的变量,应尽量多的在可以的地方使用int型变量。
另外,许多嵌入式处理器有16位乘法指令,而缺少32位乘法指令。在这种情况下,32位乘法将被仿效执行,一般情况下都是很慢的。如果数据被执行乘法作业并且运算结果不会超过16位的精密度,那么就使用short或者unsigned short变量。
7. 不要用不直接的调用
这是透过包含传递参数的函数指针的调用,因为那会产生不可预知的边际效应(如修改全局变量),使最佳化难以进行。
8. 编写返回数值的函数
9. 传递变量时使用数值而不是指针或者全局变量
传递大结构的数据时才使用指针。每个透过数值被传递的结构都应该在函数调用入口处被完全拷贝储存过。
10. 使用变量地址
因为本地变量的地址会引起混淆,降低程序性能,与全局变量一样。
11. 用const声明指针参数
如果函数体内不会修改到指针指向的对象,就要用const声明指针参数,这样可以让编译器避免不必要的反面假设。
12. 使用数组而不是指针,考虑透过指针存取数组的程序代码:
for (i=0; i<100; i++)
     *p++ = ...
在每次循环中,*p被赋值。这种对指针对象的赋值会阻碍最佳化。某些情况下,指针指向它自己,那么这种赋值就会修改指针本身的值,这就会强迫编译器每次循环都重新加载该指针。还有,编译器不能确定这个指针不会被循环体以外所使用,所以每次循环外都要依据增量的数值更新该指针。因此,最好使用下面的程序代码:
for (i=0; i<100; i++)
     p[i] = ...
13. 编写简单易懂的程序代码
编译器擅长制作复杂的最佳化,如函数嵌入和在适当的时候循环体展开。但编译器不擅长简化程序代码,他们不会合并循环或者不用函数嵌入。在原始程序中为了支持某些处理器架构进行的手工循环体展开会降低程序的可移植性,因为这阻止了编译器自动为其它处理器架构进行正确的循环体展开和函数嵌入。
14. 避免编写参数数量可变的函数
如果一定要这么做,使用ANSI标准方法:stdarg.h.。使用数据表替代if-then-else或者switch分支处理。如考虑以下程序代码:
typedef enum { BLUE, GREEN, RED, NCOLORS } COLOR;
替代
switch ( c ) {
     case CASE0: x = 5; break;
     case CASE1: x = 10; break;
     case CASE2: x = 1; break;
}
使用
static int Mapping[NCOLORS] = { 5, 10, 1 };
...
x = Mapping[c];
15. 依靠libc函数库(比如:strcpy、strlen、strcmp、bcopy、bzero、memset和memcpy)。这些函数是经过精心最佳化的。
本文小结
编译器设计者已经开发了很多复杂的最佳化功能以使最新的处理器获得最大性能,他们还在继续开发更智能的最佳化算法。应用程序开发人员可以透过使用恰当的编程规则来尽可能多地利用编译器的这些最佳化功能。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值