如何写出高效率的C代码

在8位单片机时代,RAM往往是最紧缺的资源,这时我们需要使出浑身解数,给出聪明的数据存储、压缩、重用、编码算法以适应小小的RAM。慢慢地,当程序的功能越来越多,存储代码ROM变成最紧缺的资源,这时候我们需要复用、重构代码段、改用汇编、简化C library库以满足ROM大小的限制。

当今,RAM、ROM、CPU处理能力往往都已不是瓶颈,那21世纪什么最重要?功耗!电子产品都在朝小型化、便携化、电池驱动发展,手机充电后可以待机一周,而如有可能,我们期望它能待机一年。功耗问题往往被认为是硬件工程师的任务,但是也需要软件工程师写出高效的代码,以充分利用硬件特性实现节能节耗目标。

问题的两面 

基于ARM的嵌入式系统一般都有个实时操作系统。一般来说,操作系统开发人员和应用程序开发人员不是同一拨人,所以这两拨人都需要关注低功耗的软件设计技巧,当然他们的关注点会有所不同(如下图)。

内存的使用 

总所周知,ARM是有cache的,所以访问数据的存储位置越远,代码执行时间和功耗代价就越大。大多数系统都有某种存储体系结构,简单的如片内RAM加上一块片外RAM,复杂的如核心处理单元和外部存储系统中间有两到三层cache(如下图)。多数ARM处理器都集成了片内存储器,如片内cache、片内SRAM、紧耦合存储器(TCM)、写缓存。TCM主要是为了弥补cache访问的不确定性而增加的片内存储器。

有个简单的效率计算规则,如果访问L1 cache需要一个指令周期,在L2 cache需要10个,外部RAM需要100个,以此类推。功耗也以类似的指数规律递增。

所以cache的使用关键。如果在你的系统中还没有用,请尽快把它打开,并尽可能多的使用它。

指令个数 

在存储系统中取得指令/数据后,第二步就是执行指令。所以指令越少,功耗消耗越少;任务完成的越快,CPU使用的越少,这样,CPU就可以尽可能多的时间里处在低功耗的休眠状态。简单的说,代码越快,功耗越低。

同时,相对于内存访问,指令执行的功耗更低。所以基于CPU(有数据处理)的算法比基于内存(有数据迁徙、交换)的算法功耗更低。

就编译方式而言,基于代码大小的编译优化(更小的代码)往往比基于速度的编译优化更加有效率。这是因为代码越小,cache的使用效果越好。可见cache的优化效果是相当显著的。

数据类型和大小 

ARM是一个32位处理器,这就是说它的寄存器是32位的,ALU(算术逻辑单元)是32位的,内部数据地址是32为的,所以理所当然,他们最擅长处理32位的数据运算。16位的加法运算比32位的加法运算使用了更多的指令(如截取和符号位填充)。

虽然后来的架构版本增加的对16位数据处理的运算指令SIMD(Single Instruction Multiple Data,单指令多数据流可以),可以规避一些低效的运算处理,但是大多数情况下, C代码还是很难利用这些指令。

数据对齐

ARM核对所访问的代码和数据有严格的对齐要求。至今为止,ARM核仍无法正确执行不对齐的指令。虽然ARM核支持对不对齐的数据的访问,但是加载一个不对齐的数据,就算它只用了一条指令(早期的ARM核,需要3到4条指令),底层硬件仍需要执行多条总线操作。这些总线操作对程序和程序员是完全透明的,但是在硬件上,是真实存在并消耗时间和功耗的。

结构体和数组

数组元素的大小最好是2的阶乘,这样可以简化对元素的寻址指令。如数组元素大小为12, 基址r3,元素索引r1,则寻址汇编指令为:

ADD r1, r1, r1, LSL #1 ; r1 = 3 * r1

LDR r0, [r3, r1, LSL #2] ; r0 = *(r1 + 4 * r1)

相对的,如果数组元素大小为16, 寻址指令会更加简单:

LDR r0, [r3, r1, LSL #4] ; r0 = *(r3 + 16 * r1)

参数传递

根据AAPCS,半字类型参数通过32位寄存器或者堆栈上的32位字传递。ARM有4个寄存器支持参数传递,所以拥有少于4个的参数的函数的参数传递更加有效率。当函数参数多于4个时,超出的部分会放在堆栈中,这就增加了外部内存访问指令,增加了函数调用的开销。

双字类型参数如果通过寄存器传递,会使用2个寄存器:r0和r1,或r2和r3。所以fx(int a, double b, int c)的参数a会放在r0,b会放在r2和r3,而c只能放在堆栈中了。而fx(int a, int c, double b)的参数布局显得更加合理。

C编译器并非无所不能

为了做到无误编译,C编译器一般在最差条件下编译你的代码,但是这就减少了很多错误检查。ARM C编译器提供了一些关键词以使编译器更加聪明地理解你的代码。

__pure  表示一个函数没有二义性(任何情况下,只要参数相同,输出就相同),没有访问全局变量。

__restrict  应用于指针声明,表示不会改写此指针指向的值。

__promise 与assert()类似,向编译器保证给定的表达式是非零的。这将允许编译器基于所做保证无需优化冗余代码。

使用data cache

在按序访问数据的情况下,cache使用率最佳。如下面这段代码,数组a,b按顺序访问,cache-friendly,数组c则恰恰相反。

某些数据读写方式从cache-friendly的角度看,也是不提倡的。如当开启write-allocate(把要写的地址所在的块先从main memory调入cache中,然后写cache)功能时,写大量数据,但这些数据又不会马上被重用,cache就会被这些没有用的数据大面积污染。

全局数据访问

在ARM指令架构中,不支持32位地址的直接内存访问指令。所以访问全局变量时,其地址或基址需要放在寄存器中。如果你的函数中会访问多个外部全局数据,那么他们的访问地址需要一个一个分别加载。一个常用的解决办法就是:把常用的相关全局数据放在一个结构体中,这样就保证对这些全局数据访问时,只需要加载一次基址。

 

结论

  • 合理充分利用平台和工具的特性;
  • 合理配置cache,尽量使用寄存器和cache,减少外部存储器访问;
  • 优化指令个数,提高计算速度。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值