嵌入开发中C语言程序优化

下面是网络上收集关于ARM的C代码优化方法,在嵌入开发中应该有用: 

[声明:以下方法非本人发现和总结,均为有心之人无私贡献,谢谢他们的劳动与分享!]

========================================================================================

C数据类型 

1. C语言的程序优化与编译器和硬件系统都有关系,设置某些编译器选项是最直接最简单的优化方式

    在默认的情况下,armcc是全部优化功能,而GNU编译器的默认状态下优化都是关闭的

    ARM C编译器中定义的char类型是8位无符号的,有别于一般流行的编译器默认的char是8位有符号的。

    所以循环中用char变量和条件 i ≥ 0时,就会出现死循环。为此,可以用fsigned - char(for gcc)或者-zc(for armcc)把char改成signed。

    其他的变量类型如下:

    char         无符号8位字节数据

    short        有符号16位半字节数据

    int          有符号32位字数据

    long         有符号32位字数据

    long long    有符号64位双字数据

2. 关于局部变量 

    大多数ARM数据处理操作都是32位的,局部变量应尽可能使用32位的数据类型(int或long)就算处理8位或者16位的数值,也应避免用char和short以求边界对齐,除非是利用char或者short的数据一出归零特性(如255+1=0,多用于模运算)。否则,编译器将要处理大于short和char取值范围的情况而添加代码。

另外对于表达式的处理也要格外小心,如下例子

  1. short checksum_v3(short * data){     
  2.     unsigned int i;     
  3.     short sum = 0;     
  4.     for(i = 0; i < 64 ; i++){     
  5.     sum = (short)( sum + data );   
  6.     //这里表达式式整形的,所以返处理非32位数据时,               
  7.     //要小心处理数据类型的转换。         
  8.     //原来short+short=int 但 int +int=int。。奇怪的处理     
  9.     }     
  10.     return sum;   
  11. }    
同时如上例的程序所示,这样在循环体中的每次运算都要进行类型转换,会降低程序的效率,可以先把其当作int来运算,然后再返回一个short类型。   
同时,由于处理的data[]是一个short型数组,用LDRH指令的话,不能使用桶型移位器,所以只能先进行偏移量的以为操作,然后再寻址,也会造成不佳的性能。解决的方法是用指针代替数组操作。如下:

  1. short checksum_v4(short * data){   
  2.     unsigned int i;     
  3.     int sum = 0;   
  4.     for( i = ; i<64; i++)   {   
  5.     sun += ( data ++);   
  6.     }   
  7.     return (short) sum;   
  8. }   


3. 关于函数参数类型   
函数参数和返回值应尽量使用int类型。 
另外,对于调用频率较低的全局变量,尽量使用小的数据类型以节省空间。

C循环结构 

◎ 使用减数到零的循环体,以节省指令和寄存器的使用。  
◎ 使用无符号的循环计数值,并用条件 i != 0中止。  
◎ 如果循环体至少执行一次,用优先选用do-while。  
◎ 适当情况下展开循环体。  
◎ 尽量使用数组的大小是4或8的备述,用此倍数展开循环体 寄存器分配  
◎ 尽量限制函数内部循环所用局部变量的数目,最多不超过12个,以便编译器能把变量分配到寄存器。  

◎ 可以引导编译器,通过查看是否属于最内层循环的便赖宁嘎来去定某个变量的重要性。

函数调用 

ARM中的函数前4个整型参数通过寄存器r0、r1、r2、r3来传递,随后的整型参数通过堆栈来传递。(full desceding stack)。 

◎ 尽量限制函数参数,不要超过四个,也可以把相关的参数组织在结构体传递。 
◎ 把比较小的被调用函数和调用函数放在同一个源文件中,并且限定以后调用,编译器能进行优化。 
◎ 用_inline内联性能影响较大的重要函数。

指针别名 

◎ 用一个局部变量来保存公共子表达式的值,保证该表达式只求一次值。 
◎ 避免使用局部变量的地址,否则访问这个变量的效率较低。

结构体的安排 

◎ 小的元素放在结构体的开始,大的元素放在结构体的最后 
◎ 避免使用过大的结构体,用层次话的小结构体代替。 
◎ 人工对API的结构体增加填充位以提高移植性。 
◎ 枚举类型要慎用,因为它的大小与编译器相关。

位域

◎ 尽量用define或者enum来代替位域 
◎ 用逻辑运算来丢位域操作 边界不对齐数据和字节排列方式 
◎ 尽量避免使用边界不对齐数据; 
◎ 用char× 可指向任意字节对齐的的数据,与逻辑运算配合,可访问任意边界和排列的数据。

除法 

◎ 一堆算法,不好写,总的来说是以乘代除,配合移位运算。 内联函数和内嵌汇编 
◎ 没什么好写的,就是内联减少调用开销,内嵌汇编提高运行效率。

总结

总的来说,高级语言的优化和编译器、硬件结构有关。

硬件上,ARM一般为32位总线,以32位访问数据的速度较快。局部变量和其他常用的变量要尽量利用32位的int类型,组织结构体时,也要注意元素的位置(小前大后),以节省空间。另外,由于ARM指令可条件执行,所以充分利用cpsr会使程序更有效率。同时注意好类型之间的运算,尽量减少转型操作。任何时候除法和取模运算可以同时取得结果而不会额外增加运算过程,但单单对于除法,还是以乘代除比较划算。

对于编译器,armcc遵从ATPCS的要求,第一到第四个参数依次通过r0~r4传递,其他参数通过堆栈传递,返回值用r0传递,因此,为了把大部分操作放在寄存器中完成,参数最好不多与4个。另外,可用的通用寄存器有12个,所以尽量将局部变量控制在12个之内,效率上会得到提升。同时,由于编译器比较保守,指针别名会引起多余的读操作,所以尽量少用。

============================================================================================


数据类型

存放在寄存器中的局部变量(尤其是循环变量)应尽可能使用32位数据类型int(=long),8位变量不节省任何空间和时间;

即使传输一个8位数据,函数参数和返回值使用32位类令会更有效;

能用指针递增寻址就不用数组下表递增寻址a=data[i++]不如a=*(data++);

除法运算使用无符号数更快;

存放在存储器中的数组和全局变量,尽可能使用小尺寸数据类型;

short型数组尽量避免使用数组基地址的偏移量,因为LDRH指令不支持偏移寻址;

存储器变量和寄存器变量相互赋值时使用显式类型转换,其他情况下避免不必要的类型转换;

循环结构

采用减计数循环比增计数循环更好,终止条件尽量写 i != 0 ;循环变量起始值是变量且不等于0的情况下用do-while循环更优(终止条件在后);

若循环体过于简单,比如少于4个周期,可展开循环体(重复写几遍循环体代码),以免循环体代码还不如循环本身执行周期长;

尽量限制函数内部循环所用局部变量的数据,最多不要超过12个,这样编译器就可以把他们都分配给ARM寄存器;

函数调用

尽量限制函数的参数,不要超过4个。可以将几个相关参数组织在一个结构体中;

把较小的被调函数和调用函数放在一个文件中,并先定义再调用;

对性能影响较大的重要函数可使用_inline进行内联;

指针别名

  • 建立一个新的局部变量来保存包含存储器访问的表达式,这样可以保证只对这个表达式求一次值,例如int a=data[n];b+=a;c+=a; 比b+=data[n];c+=data[n];好避免使用局部变量的地址,否则对这个变量的访问效率会比较低;
  • 结构体安排

  • 结构体元素要按照元素从小到大排序;
  • 避免使用很大的结构体,可以用层次化的小结构体来代替

  • 注:针对ARMv4以上版本
  • ===========================================================================================
  • 变量定义
  • 32位ARM处理器的指令集支持有符号/无符号的8位、16位、32位整型和浮点型变量类型,这不仅可以节省代码,而且可以提高代码的运行效率。按照作用范围的不同,C语言的变量可以划分为全局变量和局部变量。ARM编译器通常将全局变量定位在存储空间中,局部变量分配给通用寄存器。
  • 在全局变量声明时,需要考虑最佳的存储器布局,使得各种类型的变量能以32位的空间位基准对齐,从而减少不必要的存储空间浪费,提高运行效率。如:
    1. char   a;          char   a;  
    2. short  b;          char   c;  
    3. char   c;          short  b;  
    4. int    d;          int    d;  
  • 对于局部变量,要尽量不使用32位以外的变量类型。当一个函数的局部变量数目不多时,编译器会把局部变量分配给内部寄存器,每个变量占一个32位的寄存器。这样short和char类型的变量不但起不到节省空间的作用,反而会耗费更多的指令周期来完成short和char的存取操作。C语言代码及其编译结果如下所示:

    1. int  wordinc(int a)          wordinc:  
    2. {  
    3.     return (a+1);                   ADD  a1, a1,#1  
    4. }  
    5.   
    6. short  shortinc(short  a)               shortinc:  
    7. {                                               ADD  a1, a1,#1  
    8.     return a+1;                                 MOV  a1, a1, LSL #16  
    9. }                                               MOV  a1, a1, ASR #16  
    10.                         MOV  PC,LR  
    11.   
    12. char charinc(char a)                     charinc:  
    13. {                                               ADD  a1, a1,#1  
    14.     return a+1;                                 AND  a1, a1,#&FF  
    15. }                                               MOV  PC,LR  
    条件执行 
  • 条件执行是程序中必不可少的基本操作。典型的条件执行代码序列是由一个比较指令开始的,接下来是一系列相关的执行语句。ARM中的条件执行是通过对运算结果标志位进行判断实现的,一些带标志位的运算结果中,N和Z标志位的结果与比较语句的结果相同。尽管在C语言中没有带标志位的指令,但在面向ARM的C语言程序中,如果运算结果是与0作比较,编译器会移去比较指令,通过一条带标志位指令实现运算和判断。例如:

    1. int g(int x, int y) g:  
    2. {             ADDS a1, a1, a2  
    3.     if(x+y<0)          MOVPL a1, #0  
    4.     return 1;     MOVMI a1, #1  
    5.     else          MOV   pc, lr  
    6.     return 0;  
    7. }  
    因此,面向ARM的C语言程序设计的条件判断应当尽量采用“与0比较”的形式。C语言中,条件执行语句大多数应用在if条件判断中,也有应用在复杂的关系运算(<,==,>等)及位操运算(&&,!,and等)中的。面向ARM的C语言程序设计中,有符号型变量应尽量采取x&lt;0、x>=0、x==0、x!=0的关系运算;对于无符号型的变量应采用x==0、x!=0(或者x>0)关系运算符。编译器都可以对条件执行进行优化。
  • 对于程序设计中的条件语句,应尽量简化if和else判断条件。与传统的C语言程序设计有所不同,面向ARM的C语言程序设计中,关系表述中类似的条件应该集中在一起,使编译器能够对判断条件进行优化。
    循环 
  • 循环是程序设计中非常普遍的结构。在嵌入式系统中,微处理器执行时间在循环中运行的比例较大,因此关注循环的执行效率是非常必要的。除了在保证系统正确工作的前提下尽量简化核循环体的过程以外,正确和高效的循环结束标志条件也非常重要。按照以上所述的“与0比较”原则,程序中的循环结束条件应该是“减到0”的循环,结束条件尽量简单。应尽可能在关键循环中采取上述的判断形式,这样可以在关键循环中省去一些不必要的比较语句,减少不必要的开销,提高性能。如下面二个示例:

    1. int fact1(int n)                 int fact2(int n)           
    2. {                                {                     
    3.     int a, fact=1;                   int a, fact=1;       
    4.     for(a=1; a<=n; a++)               for(a=n; a!=0; a++)  
    5.         fact *= a;                       fact *= a;         
    6.     return(fact);                    return(fact);        
    7. }                                }      
    fact1和fact2中通过定义局部变量a来减少对n的load/store操作。fact2函数遵循了“与0比较”原则,省去了fact1编译结果中的比较指令,并且,变量n在整个循环过程不参与运算,也不需要保存。由于省去了寄存器分配,从而给其他部分程序的编译带来了方便,提高了运行效率。 
  • “减到0”的方法同样适用于while和do语句。如果一个循环体只循环几次,可以用展开的方法提高运行效率。当循环展开后,不需要循环计数器和相关的跳转语句,虽然代码的长度有所增加,但是得到了更高的执行效率。 
    除法和求余
    ARM指令集中没有提供整数的除法,除法是由C语言函数库中的代码(符号型_rt_sdiv和无符号型的_rt_udiv)实现的。一个32位数的除法需要20~140个周期,依赖于分子和分母的取值。除法操作所用的时间是一个时间常量乘每一位除法所需要的时间:

    Time(分子/分母)=C0+C1×log2(分子/分母) 
    =C0+C1×(log2(分子)-log2(分母)) 
    由于除法的执行周期长,耗费的资源多,程序设计中应当尽量避免使用除法。以下是一些避免调用除法的变通办法:
  • (1)在某些特定的程序设计时,可以把除法改写为乘法。例如:(x/y)>z,在已知y是正数而且y×z是整数的情况下,就可以写为x>(z×y)。
    (2)尽可能使用2的次方作为除数,编译器使用移位操作完成除法,如128就比100更加适合。在程序设计中,使用无符号型的除法要快于符号型的除法。
    (3)使用求余运算的一个目的是为了按模计算,这样的操作有时可以使用if的判断语句来完成,考虑如下的应用: 
    (4)对于一些特殊的除法和求余运算,采用查找表的方法也可以获得很好的运行效果。
    在除以某些特定的常数时,编写特定的函数完成此操作会比编译产生的代码效率高很多。ARM的C语言库中就有二个这样的符号型和无符号型数除以10的函数,用来完成十进制数的快速运算。在toolkit子目录的examples\explasm\div.c和examples\thumb\div.c文件中,有这二个函数的ARM和Thumb版本。 


    ==========================================================================================
    1 程序运行速度优化
    程序运行速度优化的方法可分为以下ARM几大类。
    1.1 通用的优化方法 
    (1)减小运算强度
    利用左/ 右移位操作代替乘/ 除2 运算:通常需要乘以ARM或除以2 的幂次方都可以通过左移或右移n 位来完成。实际上乘以任何一个整数都可以用移位和加法来代替乘法。ARM 7 中加法和移位可以通过一条指令来完成,且执行时间少于乘法指令。例如: i = i × 5 可以用i = (i<<2) + i 来代替。 
    利用乘法代替乘方运算:ARM7 核中内建有32 ×8 ARM乘法器, 因此可以通过乘法运算来代替乘方运算以节约乘方函数调用的开销。例如: i = pow(i, 3.0) 可用 i = i×i × i 来代替。 
    利用与运算代替求余运算:有时可以通过用与(AND )指令代替求余操作(% )来提高效率。例如:i = i % 8 可以用 i = i & 0x07 来代替。
    (2)优化循环终止ARM条件
    在一个循环结构中,循环的终止条件将严重影响着循环的效率,再加上ARM 指令的条件执行特性,所以在书写循环的终止条件时应尽量使用count-down-to-zero结构。这样编译器可以用一条BNE (若非零则跳转)指令代替CMP (比较)和BLE (若小于则跳转)两条指令,既减小代码尺寸,又加快了运行ARM速度。
    (3)使用inline 函数 
    ARM C 支持 inline 关键字,如果一个函数被设计ARM成一个inline 函数,那么在调用它的地方将会用函数体来替代函数调用语句, 这样将会彻底省去函数调 1.2 处理器相关的优化ARM方法 用的开销。使用inline 的最大缺点是函数在被频繁调用时,代码量将增大。
    (1)保持流水线畅通
    从前面的介绍可知,流水线延迟或阻断会对处理器的性能造成影响,因此应该尽量保持流水线畅通。流水线延迟难以避免, 但可以利用延迟周期进行其它ARM操作。
    LOAD/STORE 指令中的自动索引(auto-indexing)功能就是为利用ARM流水线延迟周期而设计的。当流水线处于延迟周期时, 处理器的执行单元被占用, 算术逻辑单元ARM(ALU )和桶形移位器却可能处于空闲状态,此时可以利用它们来完成往基址寄存器上加一个偏移量的操作,供后面的指令使用。例如:指令 LDR R1, [R2], #4 完成 R1= *R2 及 R2 += 4 两个操作,是后索引(post-indexing)的例子;而指令 LDR R1, [R2, #4]! 完成 R1 = *(R2 + 4) 和 R2 +=4 两个操作,是前索引(pre-indexing)的例子。 
    流水线阻断的情况可通过循环拆解等方法加以改善。一个循环可以考虑拆解以减小跳转指令在循环指令中所占的比重, 进而提高代码效率。下面以一个内存复制函数加以ARM说明。

    1. void memcopy(char *to, char *from, unsigned int nbytes)   
    2. {   
    3.     while(nbytes--)ARM   
    4.         *to++ = *from++;   
    5. }   
    为简单起见,这里假设nbytes 为16 的ARM倍数(省略对余数的处理)。上面的函数每处理一个字节就要进行一次判断和跳转, 对其中的循环体可作如下拆解:

    1. void memcopy(char *to, char *from, unsigned int nbytes)   
    2. {   
    3.     while(nbytes) {   
    4.         *to++ = *from++;   
    5.         *to++ = *from++;ARM   
    6.         *to++ = *from++;   
    7.         *to++ = *from++;   
    8.         nbytes - = 4;   
    9.      }   
    10. }  
    这样一来, 循环体中的指令数增加了,循环次数却减少了。跳转指令ARM带来的负面影响得以削弱。利用ARM 7 处理器32 位字长的特性, 上述代码可进一步作如下调整:

    1. void memcopy(char *to, char *from, unsigned int nbytes)ARM   
    2. {   
    3.     int *p_to = (int *)to;   
    4.     int *p_from = (int *)from;   
    5.     while(nbytes) {   
    6.         *p_to++ = *p_from++;   
    7.         *p_to++ = *p_from++;   
    8.         *p_to++ = *p_from++;   
    9.         *p_to++ = *p_from++;   
    10.         nbytes - = 16;   
    11.     }   
    12. }  
    经过优化后,一次循环可以处理16 个字节。跳转指令带来的影响ARM进一步得到减弱。不过可以看出, 调整后的代码在代码量方面有所增加。

    (2)使用寄存器变量
    CPU 对寄存器的存取要比对内存的存取快得多ARM, 因此为变量分配一个寄存器, 将有助于代码的优化和运行效率的提高。整型、指针、浮点等类型的变量都可以分配寄存器; 一个结构的部分或者全部也可以分配寄存器。给循环体中需要频繁访问的变量分配寄存器也能在一定程度上提高程序效率。

    1.3 指令集相关的优化方法
    有时可以利用ARM7 指令集的特点对程序ARM进行优化。
    (1)避免除法
    ARM 7 指令集中没有除法指令,其除法是通过调用C 库函数实现的。一个32 位的除法通常需要20~140 个时钟周期。因此, 除法成了一个程序效率的瓶颈, 应尽量避免使用。有些除法可用乘法代替,例如: if ( (x / y) > z)可变通为 if ( x > (y × z)) 。在能满足精度,且存储器空间冗余的情况下, 也可考虑使用查表法代替除法。当除数为2 的ARM幂次方时, 应用移位操作代替除法。
    (2)利用条件执行
    ARM 指令集的一个重要特征就是所有的指令均可包含一个可选的条件码。当程序状态寄存器(PSR )中的条件码标志满足指定条件时, 带条件码的指令才能执行。利用条件执行通常可以省去单独的判断ARM指令,因而可以减小代码尺寸并提高程序效率。
    (3)使用合适的变量类型
    ARM 指令集支持有符号/ 无符号的8 位、16 位、32位整型及浮点型变量。恰当的使用变量的类型,不仅可以节省代码,并且可以提高代码运行效率。应该尽可能地避免使用char、short 型的ARM局部变量,因为操作8 位/16 位局部变量往往比操作3 2 位变量需要更多指令, 请对比下列3 个函数和它们的汇编代码。

    1.4 存储器相关的优化方法
    (1)用查表代替计算
    在处理器资源紧张而存储器资源相对富裕的情况下, 可以用牺牲存储空间换取运行速度的办法。例如需要频繁计算正弦或余弦函数值时,可预先将函数值计算出来置于内存中供以后ARM查找。 
    (2)充分利用片内RAM
    一些厂商出产的ARM 芯片内集成有一定容量的RAM,如Atmel 公司的AT91R40807 内有128KB 的RAM,夏普公司的LH75400/LH75401 内有32KB 的RAM。处理器对片内RAM 的访问速度要快于对外部RAM 的访问,所以应尽可能将程序调入片内RAM 中运行。若因程序太大无法完全放入片内RAM ,可考虑ARM将使用最频繁的数据或程序段调入片内RAM 以提高程序运行效率。

    1.5 编译器相关的优化方法
    多数编译器都支持对程序速度和程序大小的优化,有些编译器还允许用户选择可供优化的内容及优化的程度。相比前面的各种优化方法, 通过设置编译器选项对程序进行优化不失为一种简单有效的途径。
     
    2 代码尺寸优化
    精简指令集计算机的一个重要特点是指令长度固定, 这样做可以简化指令译码的过程,但却容易导致代码尺寸增加。为避免这个问题,可以考虑采取以下措施来缩减程序ARM代码量。
    2.1 使用多寄存器操作指令
    ARM 指令集中的多寄存器操作指令LDM/STM 可以加载/ 存储多个寄存器,这在保存/ 恢复寄存器组的状态及进行大块数据复制时非常有效。例如要将寄存器R4~R12 及R14 的内容保存到堆栈中,若用STR 指令共需要10 条,而一条STMEA R13!, {R4 ?? R12, R14} 指令就能达到相同的目的,节省的指令存储空间相当可观。不过需要注意的是, 虽然一条LDM/STM 指令能代替多条LDR/STR 指令,但这并不意味着程序运行速度得到了ARM提高。实际上处理器在执行LDM/STM 指令的时候还是将它拆分成多条单独的LDR/STR 指令来执行
    2.2 合理安排变量顺序
    ARM 7 处理器要求ARM程序中的32 位/16 位变量必须按字/ 半字对齐,这意味着如果变量顺序安排不合理, 有可能会造成存储空间的浪费。例如:一个结构体中的4个32 位int 型变量i1 ~ i4 和4 个8 位char 型变量c1 ~ c4,若按照i1、c1、i2、c2、i3、c3、i4、c4 的顺序交错存放时, 由于整型变量的对齐会导致位于2 个整型变量中间的那个8 位char 型变量实际占用32 位的存储器,这样就造成了存储空间的浪费。为避免这种情况, 应将int 型变量和char 型变量按类似i1、i2、i3、i4、c1、c2、c3、c4 的顺序连续存放。 
    2.3 使用Thumb 指令 
    为了从根本上有效ARM降低代码尺寸,ARM 公司开发了16 位的Thumb 指令集。Thumb 是ARM 体系结构的扩充。Thumb 指令集是大多数常用32 位ARM 指令压缩成16 位宽指令的集合。在执行时,16 位指令透明的实时解压成32 位ARM 指令并没有性能损失。而且程序在Thumb状态和ARM 状态之间切换是零开销的。与等价的32 位ARM 代码相比,Thumb 代码节省的存储器空间可高达35% 以上。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值