通过一定的风格来编写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个参数的函数来说,编译器可以用寄存器传递所有的参数;而