当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]本文将会介绍如何优化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代码,你学会了吗?

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

2024年4月11日,中国——意法半导体的ST25R100近距离通信(NFC)读取器芯片独步业界,集先进的技术功能、稳定可靠的通信连接和低廉的成本价格于一身,在大规模制造的消费电子和工控设备内,可以提高非接触式互动功能的...

关键字: 嵌入式 数据读取器 芯片

单片机是一种嵌入式系统,它是一块集成电路芯片,内部包含了处理器、存储器和输入输出接口等功能。

关键字: 单片机 编写程序 嵌入式

Arm CPU正在从根本上推动AI变革,并造福地球。Arm架构是未来AI计算的基石。​

关键字: ARM AI

深圳2024年4月23日 /美通社/ -- 全球AI解决方案与工业级存储领导品牌宜鼎国际 (Innodisk)持续深化边缘AI布局,今(23)日发表全球首创"MIPI over Type-C"独家技术,让旗下嵌入式相机模...

关键字: AI 嵌入式 相机

为增进大家对嵌入式主板的认识,本文将对嵌入式主板以及嵌入式主板常见问题及其解决方法予以介绍。

关键字: 嵌入式 指数 主板

为增进大家对嵌入式系统的认识,本文将对嵌入式系统、嵌入式系统的特点予以介绍。

关键字: 嵌入式 指数 嵌入式系统

为增进大家对嵌入式的认识,本文将对嵌入式、嵌入式工作相关的内容予以介绍。

关键字: 嵌入式 指数 嵌入式技术

近日,Arm推出了Arm® Ethos™-U85神经网络处理器(NPU)和Arm Corstone™-320物联网参考设计平台,旨在满足海量的数据处理和大规模计算,加速推进边缘AI的发展进程。

关键字: ARM

机器人操作系统(ROS)驱动程序基于ADI产品而开发,因此可直接在ROS生态系统中使用这些产品。本文将概述如何在应用、产品和系统(例如,自主导航、安全气泡地图和数据收集机器人)中使用和集成这些驱动程序;以及这样将如何有助...

关键字: 电机控制器 机器人 嵌入式

为了赶超云计算市场上的竞争对手,谷歌正试图通过定制的Arm服务器芯片降低云计算服务成本。

关键字: 谷歌 ARM 定制芯片
关闭
关闭