当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]本文将会介绍如何优化ARM平台的C代码,从数据类型选择、数据结构组织、局部变量选择、函数inline内联、编译器选项、循环展开、条件执行、数据操作的转化、存储器的优化、代码尺寸的优化等角度给出常用的优化方法。

本文将会介绍如何优化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系列处理器(/zixunimg/eepwimg/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% 以上。

以上就是今天分享的如何优化嵌入式平台的C代码,你学会了吗?

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭