本文介绍了ARM渠道的C代码优化方法,从数据类型挑选、数据结构组织、局部变量挑选、函数inline内联、编译器选项、循环打开、条件履行、数据操作的转化、存储器的优化、代码尺度的优化等视点给出常用的优化方法。
C数据类型
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位双字数据
局部变量尽或许选用32位数据类型
ARM 指令集支撑有符号/ 无符号的8 位、16 位、32位整型及浮点型变量。恰当的运用变量的类型,不只能够节约代码,并且能够进步代码运转功率。应该尽或许地防止运用char、short 型的ARM局部变量,因为操作8 位/16 位局部变量往往比操作3 2 位变量需求更多指令。 大多数ARM数据处理操作都是32位的,局部变量应尽或许运用32位的数据类型(int或long)就算处理8位或许16位的数值,也应防止用char和short以求鸿沟对齐,除非是运用char或许short的数据一出归零特性(如255+1=0,多用于模运算)。不然,编译器即将处理大于short和char取值规模的状况而添加代码。别的关于表达式的处理也要分外当心挑选数据类型,请比照下列3 个函数和它们的汇编代码。
Int wordinc(inta) wordinc
{ ADD a1,a1,#1
return a + 1; MOV pc,lr
}
shortinc
short shortinc(shorta) ADD a1,a1,#1
{ MOV a1,a1,LSL #16
return a + 1; MOV a1,a1,ASR #16ARM
} MOV pc,lr
Char charinc(chara) charinc
{ ADD a1,a1,#1
return a + 1; AND a1,a1,#&ff
} MOV pc,lr
能够看出, 操作3 2 位变量所需的指令要少于操作8位及16 位变量。别的关于16-bit数据的加载 用LDRH指令的话,不能运用桶型移位器,所以只能先进行偏移量的认为操作,然后再寻址(能用指针递加寻址就不必数组下表递加寻址a=data[i++]不如a=*(data++)),也会形成欠安的功用。可是用指针替代数据操作就能够躲避这个问题。在全局变量声明时,需求考虑最佳的存储器布局,使得各种类型的变量能以32位的空间位基准对齐,然后削减不必要的存储空间糟蹋,进步运转功率。
关于函数参数类型
函数参数和返回值应尽量运用int类型。ARM中的函数前4个整型参数经过寄存器r0、r1、r2、r3来传递,随后的整型参数经过仓库来传递。因而尽量约束函数参数,不要超越四个,也能够把相关的参数组织在结构体传递。 关于比较小的被调用函数和调用函数能够放在同一个源文件中,并且限定为static调用,编译器能进行优化。用_inline内联功用影响较大的重要函数能够有用削减函数调用的额定开支。关于编译器,armcc遵照ATPCS的要求,第一到第四个参数顺次经过r0~r4传递,其他参数经过仓库传递,返回值用r0传递,因而,为了把大部分操作放在寄存器中完结,参数最好不多与4个。别的,可用的通用寄存器有12个,所以尽量将局部变量控制在12个之内,功率上会得到进步。一起,因为编译器比较保存,指针别号会引起剩余的读操作,所以尽量少用。
循环优化部分
循环是程序规划中十分遍及的结构。在嵌入式体系中,微处理器履行时刻在循环中运转的份额较大,因而重视循环的履行功率是十分必要的。除了在确保体系正确作业的前提下尽量简化核循环体的进程以外,正确和高效的循环完毕标志条件也十分重要。
* 运用减数到零的循环体,以节约指令和寄存器的运用;
* 运用无符号的循环计数值,并用条件 i != 0间断,这样编译器能够用一条BNE (若非零则跳转)指令替代CMP (比较)和BLE (若小于则跳转)两条指令,既减小代码尺度,又加快了运转ARM速度;
* 假如循环体至少履行一次,用优先选用do-while,这样编译器不会发生额定的代码来处理循环次数为0的状况;
* 恰当状况下打开循环体;尽管会添加循环的代码巨细,可是会削减循环跳转的开支;
* 尽量运用数组的巨细是4或8的备述,用此倍数打开循环体 寄存器分配;
* 尽量约束函数内部循环所用局部变量的数目,最多不超越12个,以便编译器能把变量分配到寄存器;
* 能够引导编译器,经过检查是否归于最内层循环的便赖宁嘎往来不断定某个变量的重要性;
* 用一个局部变量来保存公共子表达式的值,确保该表达式只求一次值;
* 防止运用局部变量的地址,不然拜访这个变量的功率较低;
结构体的处理
小的元素放在结构体的开端,大的元素放在结构体的最终; 防止运用过大的结构体,用层次化的小结构体替代; 人工对API的结构体添加填充位以进步移植性; 枚举类型要慎用,因为它的巨细与编译器相关。
关于位域, 尽量用define或许enum来替代位域;用逻辑运算来丢位域操作 鸿沟不对齐数据和字节摆放方法; 尽量防止运用鸿沟不对齐数据; 用char * 可指向恣意字节对齐的的数据,与逻辑运算合作,可拜访恣意鸿沟和摆放的数据。
数据运算的处理
除法和求余
ARM指令会集没有供给整数的除法,除法是由C言语函数库中的代码(符号型_rt_sdiv和无符号型的_rt_udiv)完结的。一个32位数的除法需求20~140个周期,依赖于分子和分母的取值。除法操作所用的时刻是一个时刻常量乘每一位除法所需求的时刻:
Time(分子/分母)=C0+C1×log2(分子/分母)
=C0+C1×(log2(分子)-log2(分母))
因为除法的履行周期长,消耗的资源多,程序规划中应当尽量防止运用除法。以下是一些防止调用除法的变通方法:
- 在某些特定的程序规划时,能够把除法改写为乘法。例如:(x/y)>z,在已知y是正数并且y×z是整数的状况下,就能够写为x>(z×y)。
- 尽或许运用2的次方作为除数,编译器运用移位操作完结除法,如128就比100愈加合适。在程序规划中,运用无符号型的除法要快于符号型的除法。
- 运用求余运算的一个意图是为了按模核算,这样的操作有时能够运用if的判别句子来完结,考虑如下的运用:
Uint counter2(uint count)
{
if(++count>=100) count=0;
return(count);
}
- 关于一些特别的除法和求余运算,选用查找表的方法也能够获得很好的运转作用。
在除以某些特定的常数时,编写特定的函数完结此操作会比编译发生的代码功率高许多。ARM的C言语库中就有二个这样的符号型和无符号型数除以10的函数,用来完结十进制数的快速运算。在toolkit子目录的examples\explasm\div.c和examples\thumb\div.c文件中,有这二个函数的ARM和Thumb版别。
其他运算操作
运用左/ 右移位操作替代乘/ 除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 来替代。
条件履行
条件履行是程序中必不可少的根本操作。典型的条件履行代码序列是由一个比较指令开端的,接下来是一系列相关的履行句子。ARM中的条件履行是经过对运算成果标志位进行判别完结的,一些带标志位的运算成果中,N和Z标志位的成果与比较句子的成果相同。尽管在C言语中没有带标志位的指令,但在面向ARM的C言语程序中,假如运算成果是与0作比较,编译器会移去比较指令,经过一条带标志位指令完结运算和判别。因而,面向ARM的C言语程序规划的条件判别应当尽量选用”与0比较”的方式。C言语中,条件履行句子大多数运用在if条件判别中,也有运用在杂乱的联系运算(<,==,>等)及位操运算(&&,!,and等)中的。面向ARM的C言语程序规划中,有符号型变量应尽量采纳x<0、x>=0、x==0、x!=0的联系运算;关于无符号型的变量应选用x==0、x!=0(或许x>0)联系运算符。编译器都能够对条件履行进行优化。关于程序规划中的条件句子,应尽量简化if和else判别条件。与传统的C言语程序规划有所不同,面向ARM的C言语程序规划中,联系表述中相似的条件应该会集在一起,使编译器能够对判别条件进行优化。因为ARM指令可条件履行,所以充分运用cpsr会使程序更有用率。ARM 指令集的一个重要特征便是一切的指令均可包括一个可选的条件码。当程序状况寄存器(PSR )中的条件码标志满意指定条件时, 带条件码的指令才干履行。运用条件履行一般能够省去独自的判别ARM指令,因而能够减小代码尺度并进步程序功率。
流水线优化
ARM处理器每种处理器都有自己的流水线结构,参阅ARM核流水线——ARM7,ARM9E,ARM11,Cortex-A系列处理器(http://houh-1984.blog.163.com/blog/static/311278342011111083852771/).流水线推迟或阻断会对处理器的功用形成影响,因而应该尽量坚持流水线疏通。流水线推迟难以防止, 但能够运用推迟周期进行其它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)的比如。流水线阻断的状况可经过循环打开,参加其它的操作等方法加以改进。一个循环能够考虑打开unroll以减小跳转指令在循环指令中所占的比重, 从而进步代码功率。下面以一个内存仿制函数加以ARM阐明。
void memcopy(char *to, char *from, unsigned int nbytes)
{
while(nbytes–)ARM
*to++ = *from++;
}
为简略起见,这儿假定nbytes 为16 的ARM倍数(省掉对余数的处理)。上面的函数每处理一个字节就要进行一次判别和跳转, 对其间的循环体可作如下打开:
void memcopy(char *to, char *from, unsigned int nbytes)
{
while(nbytes) {
*to++ = *from++;
*to++ = *from++;
*to++ = *from++;
*to++ = *from++;
nbytes – = 4;
}
}
这样一来, 循环体中的指令数添加了,循环次数却削减了。跳转指令ARM带来的负面影响得以削弱。运用ARM 7 处理器32 位字长的特性, 上述代码可进一步作如下调整:
void memcopy(char *to, char *from, unsigned int nbytes)
{
int *p_to = (int *)to;
int *p_from = (int *)from;
while(nbytes) {
*p_to++ = *p_from++;
*p_to++ = *p_from++;
*p_to++ = *p_from++;
*p_to++ = *p_from++;
nbytes – = 16;
}
}
经过优化后,一次循环能够处理16 个字节。跳转指令带来的影响ARM进一步得到削弱。不过能够看出, 调整后的代码在代码量方面有所添加。
存储器相关的优化方法
其他选用存储相关的操作能加快程序运转,如用查表替代核算。在处理器资源严重而存储器资源相对殷实的状况下, 能够用献身存储空间交换运转速度的方法。例如需求频频核算正弦或余弦函数值时,可预先将函数值核算出来置于内存中供今后ARM查找。充分运用片内ARM芯片内的高速RAM,即ARM芯片内的指令和数据TCM 或许L1 RAM和L2 RAM。处理器对片内RAM 的拜访速度要快于对外部RAM 的拜访,所以应尽或许将程序调入片内RAM 中运转。若因程序太大无法彻底放入片内RAM ,可考虑ARM将运用最频频的数据或程序段调入片内RAM 以进步程序运转功率。这便是Cache的概念,还能够经过优化数据和代码的组织来进步数据和代码的拜访功率。
代码尺度优化
精简指令集核算机的一个重要特点是指令长度固定, 这样做能够简化指令译码的进程,但却简单导致代码尺度添加。为防止这个问题,能够考虑采纳以下办法来减缩程序ARM代码量。
1)、运用多寄存器操作指令
ARM 指令会集的多寄存器操作指令LDM/STM 能够加载/ 存储多个寄存器,这在保存/ 康复寄存器组的状况及进行大块数据仿制时十分有用。例如要将寄存器R4~R12 及R14 的内容保存到仓库中,若用STR 指令共需求10 条,而一条STMEA R13!, {R4 ?? R12, R14} 指令就能到达相同的意图,节约的指令存储空间相当可观。不过需求留意的是, 尽管一条LDM/STM 指令能替代多条LDR/STR 指令,但这并不意味着程序运转速度得到了ARM进步。实践上处理器在履行LDM/STM 指令的时分仍是将它拆分红多条独自的LDR/STR 指令来履行。
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 的次序接连寄存。
3)、 运用Thumb 指令
为了从根本上有用ARM下降代码尺度,ARM 公司开发了16 位的Thumb 指令集。Thumb 是ARM 体系结构的扩大。Thumb 指令集是大多数常用32 位ARM 指令压缩成16 位宽指令的调集。在履行时,16 位指令通明的实时解压成32 位ARM 指令并没有功用丢失。并且程序在Thumb状况和ARM 状况之间切换是零开支的。与等价的32 位ARM 代码比较,Thumb 代码节约的存储器空间可高达35% 以上。