ARM下高效C编程

 

通过一定的风格来编写 C 程序,可以帮助 C 编译器生成执行速度更快的 ARM 代码。下面就是一些与性能相关的关键点:
       1 、对局部变量、函数参数和返回值要使用 signed unsigned int 类型。这样可以避免类型转换,而且可高效地使用 ARM 32 位数据操作指令。
       2 、最高效的循环体形式是减计数到零( counts down to zero )的 do-while 循环。
       3 、展开重要的循环来减少循环的开销。
       4 、不要依赖编译器来优化掉重复的存储器访问。指针别名会阻止编译器的这种优化。
       5 、尽可能把函数参数的个数限制在 4 个以内。如果函数参数都存放在寄存器内,那么函数调用就会快得多。
       6 、按元素尺寸从小到大排列的方法来安排结构体,特别是在 thumb 模式下编译。
       7 、不要使用位域,可以用掩码和逻辑操作来替代。
       8 、避免除法,可以用倒数的乘法来替代。
       9 、避免边界不对齐的数据。如果数据有可能边界不对齐,那么就要使用 char * 指针类型来访问。
       10 、在 C 编译器中使用内嵌汇编可以利用到 C 编译器本来不支持的指令或优化。
      
一、        数据类型使用上的优化
1 、局部变量
一个 char 类型的数据比 int 类型的数据占用更小的寄存器空间或者更小的 ARM 堆栈空间。这两种设想对于 ARM 来说,都是错误的。所有的 ARM 寄存器都是 32 位的,所有的堆栈入口至少是 32 位的。当我们执行 i++ ,要利用当 i=255 后, i++=0 这个条件时,可以把它定义为 char 类型。
       2 、函数参数
              尽管宽和窄的函数调用规则各有其优点,但 char short 类型的函数参数和返回值都会产生额外的开销,导致性能的下降,并增加了代码尺寸。所以,即使是传输一个 8 位的数据,函数参数和返回值使用 int 类型也会更有效。      
       3 、总结
              1 )对于存放在寄存器中的局部变量,除了 8 位或 16 位的算术模运算外,尽量不要使用 char short 类型,而要使用有符号或无符号 int 类型。除法运算时使用无符号数执行速度更快。
              2 )对于存放在主存储器中的数组和全局变量,在满足数据大小的前提下,应尽可能使用小尺寸的数据类型,这样可以节省存储空间。 ARMv4 体系结构可以有效地装载和存储所有宽度的数据,并可以使用递增数组指针来有效地访问数组。对于 short 类型数组,要避免使用数组基地址的偏移量,因为 LDRH 指令不支持偏移寻址。
              3 )通过读取数组或全局变量并赋给不同类型的局部变量时,或者把局部变量写入不同类型的数组或者全局变量时,要进行显式数据类型转换。这种转换使编译器可以明确、快速地处理,把存储器中数据宽度比较窄的数据类型扩展,并赋给寄存器中较宽的类型。
              4 )由于隐式或者显式的数据类型转换通常会有额外的指令周期开销,所以在表达式中应尽量避免使用。 Load store 指令一般不会产生额外的转换开销,因为 load store 指令是自动完成数据类型转换的。
              5 )对于函数参数和返回值应尽量避免使用 char short 类型。即使参数范围比较小,也应该使用 int 类型,以防止编译器做不必要的类型转换。
二、 C 循环结构
ARM 上,一个循环其实只要 2 条指令就足够了:
一条减法指令,进行循环减法计数,同时设置结果的条件标志;
一条条件分支指令。
这里的关键是,循环的终止条件应为减计数到零,而不是计数增加到某个特定的限制值。由于减计数结构已存储在条件标志里,与零比较的指令就可以省略了。由于不用 i 作为数组的下标索引,采用减计数就没有任何问题了。
总而言之,无论对于有符号的循环计数值,都应使用 i =0 作为循环的结束条件。 对有符号数 i ,这比使用条件 i>0 少了一条指令。
总结:
1   使用减计数到零的循环结构,这样编译器就不需要分配一个寄存器来保存循环终止值,而且与 0 比较的指令也可以省略。
2   使用无符号的循环计数值,循环继续的条件为 i!=0 而不是 i>0 ,这样可以保证循环开销只有两条指令。
3   如果事先知道循环体至少会执行一次,那么使用 do-while 循环要比 for 循环要好,这样可以使编译器省去检查循环计数值是否为零的步骤。
4   展开重要的循环体可降低循环开销,但不要过度展开,如果循环的开销对整个程序来说占的比例很小,那么循环展开反而会增加代码量并降低 cache 的性能。
5   尽量使数组的大小是 4 8 的倍数,这样可以容易的以 2 4 8 次等多种选择展开循环,而不需要担心剩余数组元素的问题。
三、          寄存器分配
高效的寄存器分配
应该尽量限制函数内部循环所用局部变量的数目,最多不超过 12 个,这样,编译器就可以把这些变量都分配给 ARM 寄存器。
四、          函数调用
4 寄存器规则
带有 4 个或者更少参数的函数,要比多于 4 个参数的函数执行效率高得多。对带有少于 4 个参数的函数来说,编译器可以用寄存器传递所有的参数;而对于多于 4 个参数的函数,函数调用者和被调用者必须通过访问堆栈来传递一些参数。
如果函数体积很小,只用到很少的寄存器,那么还有一些其他的方法来减少函数调用的开销。可以把调用函数和被调用函数放在同一个 C 文件中,这样编译器就知道了被调用函数生成的代码,并以此对调用函数进行一些优化。
总结:
1   尽量限制函数的参数,不要超过 4 个,这样函数调用的效率会更高。也可以将几个相关的参数组织在一个结构体中,用传递结构体指针来代替多个参数。
2   把比较小的被调用函数和调用函数放在同一个源文件中,并且要先定义,后调用,编译器就可以优化函数调用或者内联较小的函数。
3   对性能影响较大的重要函数可使用关键字 _inline 进行内联。
五、          指针别名
定义:当 2 个指针指向同一个地址对象时,这 2 个指针被称作该对象的别名( alias )。如果对其中一个指针进行写入,就会影响从另一个指针的读出。在一个函数中,编译器通常不知道哪一个指针是别名,哪一个不是;或哪一个指针有别名,哪一个没有。
避免指针别名:
1   不要依赖编译器来消除包含存储器访问的公共子表达式,而应建立一个新的局部变量来保存这个表达式的值,这样可以保证只对这个表达式求一次值;
2   避免使用局部变量的地址,否则对这个变量的访问效率会比较低。
六、        结构体安排
ARM 上使用结构体有 2 个问题需要考虑:结构体地址边界对齐和结构体总的大小。
获得高效结构体的原则:
1   把所有 8 位大小的元素安排在结构体的前面;
2   以此安排 16 位、 32 位和 64 位的元素;
3   把所有数组和比较大的元素安排在结构体最后。
4   对于一条指令,如果结构体太大而不能访问所有的元素,那么把元素组织到一个子结构体中。编译器可以维持单独的子结构体的指针。
小结:
结构体元素要按照元素的大小来排列,以最小的元素放在开始,最大的元素安排在最后;避免使用很大的结构体,可以用层次化的小结构体来代替;为了提高可移植性,人工对 API 的结构体增加填充位,这样,结构体的安排将不会依赖与编译器;在 API 的结构体中要谨慎使用枚举类型。一个枚举类型的大小是编译器相关的。
七、        位域
注意事项:
1   应避免使用位域,而使用 #define 或者 enum 来定义屏蔽位;
2   使用整型逻辑运算 AND OR 异或 操作和屏蔽对位域进行测试、取反和设置操作。这些操作编译效率高,还可以同时对多个位域进行测试、取反和设置。
八、            边界不对齐数据和字节排列方式(大 / 小端)
边界不对齐数据和字节排列方式这 2 个问题,可使内存访问和移植问题复杂化。须考虑数组指针是否边界对齐, ARM 配置是大端( big-endian ),还是小端( little-endian )的存储器系统。
总结:
1   尽量避免使用边界不对齐的数据;
2   使用类型 char * 可指向任意字节边界的数据。 通过读字节来访问数据,使用逻辑操作来组合数据,这样代码就不会依赖于边界是否对齐或者 ARM 的字节排列方式的配置;
3   为了快速访问边界不对齐的结构体,可以根据指针边界和处理器的字节排序方式写出不同的程序变体。
九、 除法
ARM 硬件上不支持除法指令,当代码中出现除法运算时, ARM 编译器会调用 C 库函数(有符号的除法调用 _rt_sdiv ,无符号的调用 _rt_udiv ),来实现除法操作。有许多不同类型的除法程序来适应不同的除数和被除数。
总结:
1   尽可能避免使用除法。对环形缓冲区的处理可以不用除法。
2   如果不能避免除法运算,那么尽可能考虑使用除法程序同时产生商 n/d 和余数 n%d 的好处。
3   对于重复对同一除数 d 的除法,预先计算好 s=(2k-1)/d 。可用乘以 s 2k 位乘法来代替除以 d k 位无符号整数除法。
4 )使用 2 的整数次幂作除数。当 2 的整数次幂做除数时,编译器会自动将除法运算转换成移位运算。所以在编写程序算法时,尽量使用 2 的整数次幂做除数。
5 )求余运算。可以将一些典型的求余运算进行转换,以避免在程序中使用除法运算。如:
uint counter1(uint count)
{
        return (++count%60);
}
转换成:
uint counter2(uint count)
{
        if (++count >=60)
        count=0;
        return (count);
}
十、        浮点运算
大多数 ARM 处理器硬件上并不支持浮点运算。这样在一个对价格敏感的嵌入式应用系统中,可节省空间和降低功耗。除了硬件向量浮点累加器 VFP ARM7500FE 上的浮点累加器 FPA 外, C 编译器必须在软件上提供浮点支持。
十一、内联函数和内嵌汇编
高效地调用函数,使用内联函数可以完全去除函数调用的开销,另外许多编译器允许在 C 源程序中使用内嵌汇编。使用包含汇编的内嵌函数,可以使编译器支持通常不能有效使用的 ARM 指令和优化方法。
       内联函数和内嵌汇编最大的好处是,可以实现一些在 C 语言部分中通常难以完成的操作。使用内联函数要比使用 #define 宏定义更好,因为后者不检查函数参数和返回值的类型。
 
目 录 第1章 ARM微处理器概述 1.1 ARM-Advanced RISC Machines 1.2 ARM微处理器的应用领域及特点 1.2.1 ARM微处理器的应用领域 1.2.2 ARM微处理器的特点 1.3 ARM微处理器系列 1.3.1 ARM7微处理器系列 1.3.2 ARM9微处理器系列 1.3.3 ARM9E微处理器系列 1.3.4 ARM10E微处理器系列 1.3.5 SecurCore微处理器系列 1.3.6 StrongARM微处理器系列 1.3.7 Xscale处理器 1.4 ARM微处理器结构 1.4.1 RISC体系结构 1.4.2 ARM微处理器的寄存器结构 1.4.3 ARM微处理器的指令结构 1.5 ARM微处理器的应用选型 1.6 本章小节 第2章 ARM微处理器的编程模型 2.1 ARM微处理器的工作状态 2.2 ARM体系结构的存储器格式 2.3 指令长度及数据类型 2.4 处理器模式 2.5 寄存器组织 2.5.1 ARM状态下的寄存器组织 2.5.2 Thumb状态下的寄存器组织 2.5.3 程序状态寄存器 2.6 异常(Exceptions) 2.6.1 ARM体系结构所支持的异常类型 2.6.2 对异常的响应 2.6.3 从异常返回 2.6.4 各类异常的具体描述 2.6.5 异常进入/退出小节 2.6.6 异常向量(Exception Vectors) 2.6.7 异常优先级(Exception Priorities) 2.6.8 应用程序中的异常处理 2.7 本章小节 第3章 ARM微处理器的指令系统 3.1 ARM微处理器的指令集概述 3.1.1 ARM微处理器的指令的分类与格式 3.1.2 指令的条件域 3.2 ARM指令的寻址方式 3.2.1 立即寻址 3.2.2 寄存器寻址 3.2.2 寄存器间接寻址 3.2.3 基址变址寻址 3.2.4 多寄存器寻址 3.2.5 相对寻址 3.2.6 堆栈寻址 3.3 ARM指令集 3.3.1 跳转指令 3.3.2 数据处理指令 3.3.3 乘法指令与乘加指令 3.3.4 程序状态寄存器访问指令 3.3.5 加载/存储指令 3.3.6 批量数据加载/存储指令 3.3.7 数据交换指令 3.3.8 移位指令(操作) 3.3.9 协处理器指令 3.3.10 异常产生指令 3.4 Thumb指令及应用 3.5 本章小节 第4章 ARM程序设计基础 4.1 ARM汇编器所支持的伪指令 4.1.1 符号定义(Symbol Definition)伪指令 4.1.2 数据定义(Data Definition)伪指令 4.1.3 汇编控制(Assembly Control)伪指令 4.1.4 其他常用的伪指令 4.2 汇编语言的语句格式 4.2.1 在汇编语言程序中常用的符号 4.2.2 汇编语言程序中的表达式运算符 4.3 汇编语言的程序结构 4.3.1 汇编语言的程序结构 4.3.2 汇编语言的子程序调用 4.3.3 汇编语言程序示例 4.3.4 汇编语言与C/C++的混合编程 4.4 本章小节 第5章 应用系统设计与调试 5.1 系统设计概述 5.2 S3C4510B概述 5.2.1 S3C4510B及片内外围简介 5.2.2 S3C4510B的引脚分布及信号描述 5.2.3 CPU内核概述及特殊功能寄存器(Special Registers) 5.2.4 S3C4510B的系统管理器(System Manager) 5.3 系统的硬件选型与单元电路设计 5.3.1 S3C4510B芯片及引脚分析 5.3.2 电源电路 5.3.3 晶振电路与复位电路 5.3.4 Flash存储器接口电路 5.3.5 SDRAM接口电路 5.3.6 串行接口电路 5.3.7 IIC接口电路 5.3.8 JTAG接口电路 5.3.9 10M/100M以太网接口电路 5.3.10 通用I/O接口电路 5.4 硬件系统的调试 5.4.1 电源、晶振及复位电路 5.4.2 S3C4510B及JTAG接口电路 5.4.3 SDRAM接口电路的调试 5.4.4 Flash接口电路的调试 5.4.5 10M/100M以太网接口电路 5.5 印刷电路板的设计注意事项 5.5.1 电源质量与分配 5.5.2 同类型信号线的分布 5.6 本章小节 第6章 部件工作原理与编程示例 6.1 嵌入式系统的程序设计方法 6.2 应用程序编程示例 6.2.1 通用I/O口工作原理与编程示例 6.2.2 串行通讯工作原理与编程示例 6.2.3 中断控制器工作原理与编程示例 6.2.4 定时器工作原理与编程示例 6.2.5 GDMA工作原理与编程示例 6.2.5 IIC总线控制器工作原理 6.2.5 以太网控制器工作原理 6.2.6 Flash存储器工作原理与编程示例 6.3 BootLoader简介 6.4 本章小节 第7章 嵌入式uClinux及其应用开发 7.1 嵌入式uClinux系统概况 7.2 开发工具GNU的使用 7.2.1 GCC编译器 7.2.2 GNU Make 7.2.3 使用GDB调试程序 7.3 建立uClinux开发环境 7.3.1 建立交叉编译器 7.3.2 uClinux针对硬件的改动 7.3.3 编译uClinux内核 7.3.4 内核的加载运行 7.4 在uClinux下开发应用程序 7.4.1 串行通信 7.4.2 socket编程 7.4.3 添加用户应用程序到uClinux 7.4.4 通过网络添加应用程序到目标系统 7.5 本章小结 第8章 ARM ADS集成开发环境的使用 8.1 ADS软件组成介绍 8.1.1 命令行开发工具 8.1.2 ARM运行时库 8.1.3 GUI开发环境(Code WarriorAXD) 8.1.4 实用程序 8.1.5 支持的软件 8.2 使用ADS创建工程 8.2.1 建立一个工程 8.2.2 编译链接工程 8.2.3 使用命令行工具编译应用程序 8.3 用AXD进行代码调试 8.4 本章小结
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值