|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
+ t7 v! y6 L2 b% p; I+ I* |, W
7 e2 ?: ?& g+ B1 i# h* J1 M
. D8 v3 }2 q% b3 h6 q5 R
通过一定的风格来编写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编译器本来不支持的指令或优化。 一、 数据类型使用上的优化:+ H1 }5 }6 f! E6 x. c5 o4 M
" s; e* ^, W0 S& e1 y1.局部变量6 v/ F4 m6 u% l# e4 \1 |; k' |5 a! z3 v' k
一个char类型的数据比int类型的数据占用更小的寄存器空间或者更小的ARM堆栈空间。这两种设想对于ARM来说,都是错误的。所有的ARM寄存器都是32位的,所有的堆栈入口至少是32位的。当我们执行i++,要利用当i=255后,i++=0这个条件时,可以把它定义为char类型。 r$ X. Q! q& v! ~0 y. J' K
! t' s- r% T+ `3 e: `+ Y4 C' | 2.函数参数2 `1 y& ^ ]2 j$ n7 W( X5 O: D* @4 `8 E0 O5 Z( G9 H
尽管宽和窄的函数调用规则各有其优点,但char或short类型的函数参数和返回值都会产生额外的开销,导致性能的下降,并增加了代码尺寸。所以,即使是传输一个8位的数据,函数参数和返回值使用int类型也会更有效。
: J: H; ~' C9 l4 g9 E9 w 总结: + U! I. J* k/ L0 Y
; e8 m0 T1 A6 R3 l 1)对于存放在寄存器中的局部变量,除了8位或16位的算术模运算外,尽量不要使用char和short类型,而要使用有符号或无符号int类型。除法运算时使用无符号数执行速度更快。 5 r& o) Q' V7 ~: N7 Z2 d' ]2 i8 Z: B" H: Q5 a1 C
2)对于存放在主存储器中的数组和全局变量,在满足数据大小的前提下,应尽可能使用小尺寸的数据类型,这样可以节省存储空间。ARMv4体系结构可以有效地装载和存储所有宽度的数据,并可以使用递增数组指针来有效地访问数组。对于short类型数组,要避免使用数组基地址的偏移量,因为LDRH指令不支持偏移寻址。 0 ]& Z- Q8 _) K" M! g, {9 Y* W9 A+ C$ N+ [' B: T; T
3)通过读取数组或全局变量并赋给不同类型的局部变量时,或者把局部变量写入不同类型的数组或者全局变量时,要进行显式数据类型转换。这种转换使编译器可以明确、快速地处理,把存储器中数据宽度比较窄的数据类型扩展,并赋给寄存器中较宽的类型。 2 ~* V6 x8 f9 s" P2 C9 _) [5 W4 l
4)由于隐式或者显式的数据类型转换通常会有额外的指令周期开销,所以在表达式中应尽量避免使用。Load和store指令一般不会产生额外的转换开销,因为load和store指令是自动完成数据类型转换的。 2 D! i! g' Y0 d4 _' C1 g4 v
' H# o; Y {& I" B; G( U 5)对于函数参数和返回值应尽量避免使用char和short类型。即使参数范围比较小,也应该使用int类型,以防止编译器做不必要的类型转换。$ K1 Z& K) i* p2 B$ m# c% q1 q2 D* k4 ^. z0 D
4 X P8 _$ m9 n5 ?. i
二、C循环结构- x1 n9 y) U2 N2 G/ R9 p- X' l7 p
. B N; y( N/ X) C4 k# m& |$ x9 }! T在ARM上,一个循环其实只要2条指令就足够了:
N8 g C+ f0 d1 S; R
0 a- y$ j5 B s- 一条减法指令,进行循环减法计数,同时设置结果的条件标志;
- 一条条件分支指令。7 ]3 Q; A* w+ M/ a3 W, x
5 |* R# ~/ M# Z" P
8 T9 b p T/ w2 w' K
& D: j, B/ N; G; t2 y: |/ q2 g3 k6 ]2 K
\) _ o8 ~8 _6 c这里的关键是,循环的终止条件应为减计数到零,而不是计数增加到某个特定的限制值。由于减计数结构已存储在条件标志里,与零比较的指令就可以省略了。由于不用i作为数组的下标索引,采用减计数就没有任何问题了。
1 |4 y6 N# `& P _8 r& Y0 C) }
/ P: }7 r5 p9 h I总而言之,无论对于有符号的循环计数值,都应使用i!=0作为循环的结束条件。对有符号数i,这比使用条件i>0少了一条指令。" z, C; R/ ^" ^! C" M' p, R B! D3 A* }- u3 G5 n7 l
0 N: n, X! z1 B4 i' `6 ?) c0 |6 ?- D) b9 G! f5 a0 ^
总结:4 p5 E0 W# u0 W- ?- X9 d: w) t# {/ M% E6 O& Q: x4 Q# H
1) 使用减计数到零的循环结构,这样编译器就不需要分配一个寄存器来保存循环终止值,而且与0比较的指令也可以省略。% y, Q- S% h- k3 E6 E
2) 使用无符号的循环计数值,循环继续的条件为i!=0而不是i>0,这样可以保证循环开销只有两条指令。+ Q( a- q* z) O3 D) L* {7 ?) H0 y. t1 G# V( h
3) 如果事先知道循环体至少会执行一次,那么使用do-while循环要比for循环要好,这样可以使编译器省去检查循环计数值是否为零的步骤。4 O1 D1 h2 I! b# i2 H8 N0 @
4) 展开重要的循环体可降低循环开销,但不要过度展开,如果循环的开销对整个程序来说占的比例很小,那么循环展开反而会增加代码量并降低cache的性能。
- {% i u3 H, {' g3 r/ ~- c5) 尽量使数组的大小是4或8的倍数,这样可以容易的以2,4,8次等多种选择展开循环,而不需要担心剩余数组元素的问题。
* o/ E1 }. `2 B2 ^" V6 i2 I; e! p& [3 ^8 I9 L! _: I" n* A4 y/ g# c4 ^3 c# |9 r
三、寄存器分配4 g' T% B& {) W' \2 k: i0 Y+ |
高效的寄存器分配:应该尽量限制函数内部循环所用局部变量的数目,最多不超过12个,这样,编译器就可以把这些变量都分配给ARM寄存器。3 G3 L4 W) ^% e
# Y. U! o" k/ m- r四、函数调用
R: `; g+ Q% @' d* c% B7 z% a* N) m+ Q! x3 \4 \
2 s! f2 |/ I6 z L4寄存器规则:带有4个或者更少参数的函数,要比多于4个参数的函数执行效率高得多。对带有少于4个参数的函数来说,编译器可以用寄存器传递所有的参数;而对于多于4个参数的函数,函数调用者和被调用者必须通过访问堆栈来传递一些参数。
/ D7 W% B$ L. h, P$ J/ q, X4 o1 m0 J8 u ` u7 x% x; D+ z4 ^& [, B: o6 H! ^* l: w
如果函数体积很小,只用到很少的寄存器,那么还有一些其他的方法来减少函数调用的开销。可以把调用函数和被调用函数放在同一个C文件中,这样编译器就知道了被调用函数生成的代码,并以此对调用函数进行一些优化。7 d: G Y, C$ U" B" u: C! k
E: b7 V, @6 C+ q1 r* N3 q3 {3 V- K
/ N5 }" X( M! w' _9 I总结:: ~5 Y2 j# M5 b6 \! e
1) 尽量限制函数的参数,不要超过4个,这样函数调用的效率会更高。也可以将几个相关的参数组织在一个结构体中,用传递结构体指针来代替多个参数。: o5 Q, \1 i% j& Y& W
2) 把比较小的被调用函数和调用函数放在同一个源文件中,并且要先定义,后调用,编译器就可以优化函数调用或者内联较小的函数。# n7 V5 w& C9 B% U( E* g/ n j7 k7 h0 C2 i* H0 E+ A
3) 对性能影响较大的重要函数可使用关键字_inline进行内联。4 V: [$ d& f9 {% ^! w( i5 ~0 }" M8 w+ B* R" A4 u
" q" @# T4 _: w5 j. |: r# w: W4 n; ^9 H& _$ w! V
五、指针别名 @- H4 n: `* v8 h' e2 Z; D
$ p& W. D ^2 y& c3 z8 T: k% y! j, K8 Z8 ^$ L4 ~8 C6 [: r Q
定义:当2个指针指向同一个地址对象时,这2个指针被称作该对象的别名(alias)。如果对其中一个指针进行写入,就会影响从另一个指针的读出。在一个函数中,编译器通常不知道哪一个指针是别名,哪一个不是;或哪一个指针有别名,哪一个没有。! H0 D0 T, \& e) B& |3 w8 L, v
' |1 |. \) s, G避免指针别名:
( C) x' e. h; o# o" \0 ?: o: D1) 不要依赖编译器来消除包含存储器访问的公共子表达式,而应建立一个新的局部变量来保存这个表达式的值,这样可以保证只对这个表达式求一次值;
0 b {: B1 v! ` k) }6 T2) 避免使用局部变量的地址,否则对这个变量的访问效率会比较低。
( z$ n* S. `7 ?+ T0 Y' f6 @- V: v4 S' E: s5 }: E3 J3 U4 c9 z
六、结构体安排2 N: ^6 s9 l. i% V( m# W
0 S( m5 j8 t2 D+ h( ~在ARM上使用结构体有2个问题需要考虑:结构体地址边界对齐和结构体总的大小。2 ?% T6 X% g) A" `: M6 e
& M$ q$ w% F7 [. G3 e. t! j- J
获得高效结构体的原则: K! O6 E7 a: }9 y! }+ t" x, v% D, Z ]
1) 把所有8位大小的元素安排在结构体的前面;+ n8 x7 K4 U0 _! [! G3 F/ c: |: e
; W: K( `; u% O# B& @2) 以此安排16位、32位和64位的元素;( U7 ?5 x7 |+ T& F8 a/ }8 f
3) 把所有数组和比较大的元素安排在结构体最后;" O" h+ t( o4 \, B7 V7 c& q3 t9 x; D I- ~ ^1 D( V+ m
4) 对于一条指令,如果结构体太大而不能访问所有的元素,那么把元素组织到一个子结构体中。编译器可以维持单独的子结构体的指针。! }4 e: D6 _: v% N: h3 l! ^
; E: R, ]$ G# A( X9 M8 d% J1 W8 A. W$ Y" q
总结:
/ j7 J9 O2 n- ?结构体元素要按照元素的大小来排列,以最小的元素放在开始,最大的元素安排在最后;避免使用很大的结构体,可以用层次化的小结构体来代替;为了提高可移植性,人工对API的结构体增加填充位,这样,结构体的安排将不会依赖与编译器;在API的结构体中要谨慎使用枚举类型。一个枚举类型的大小是编译器相关的。" m: {; h: n9 A% r6 O# j/ s. Y( {4 ^& [ ~: ?6 e# I" t& B8 x
2 _0 R' j( u2 R' |! ?+ _; e
! L8 \0 b( {* U* r七、位域8 J' ?4 X8 p. `$ ~/ d8 a! P9 a* m
注意事项:' E- k) y% U! K4 \9 O8 [, V" N7 `$ U* A* |1 e0 f
1) 应避免使用位域,而使用#define或者enum来定义屏蔽位;" h D# ~: T4 E$ f: T+ W7 D3 M$ f6 L+ i/ G! y8 N1 Z2 Q$ z
2) 使用整型逻辑运算AND、OR、“异或”操作和屏蔽对位域进行测试、取反和设置操作。这些操作编译效率高,还可以同时对多个位域进行测试、取反和设置。
- Z9 s5 W) L0 x& ?1 x4 k4 H! H" J/ |* p0 C# _
* q" P/ e- F% }1 W' @八、边界不对齐数据和字节排列方式(大/小端)1 m: ^( h b& W3 C1 _5 h [8 e
' j3 m0 {. M" Q$ {
* {# |8 N* o5 S, ^ s边界不对齐数据和字节排列方式这2个问题,可使内存访问和移植问题复杂化。须考虑数组指针是否边界对齐,ARM配置是大端(big-endian),还是小端(little-endian)的存储器系统。+ Y9 t8 N! P$ c, n; ]/ S4 b
: `0 i" z9 P. B; N
% j; w& T! P% e总结:
& N' J) n& x, e/ |3 e \$ i1) 尽量避免使用边界不对齐的数据;# z% U; p! D2 u5 o! K3 b o% C N% D
2) 使用类型char *可指向任意字节边界的数据。通过读字节来访问数据,使用逻辑操作来组合数据,这样代码就不会依赖于边界是否对齐或者ARM的字节排列方式的配置;
- n" ]2 [8 w8 A) T% u# k' F% k3) 为了快速访问边界不对齐的结构体,可以根据指针边界和处理器的字节排序方式写出不同的程序变体。
$ |4 ?( b+ v# v, r* x* a. f: \5 p0 ?; Y- r! v( p
6 U8 g" e% t5 ~6 t) X4 h九、除法
* K4 {4 _4 R9 h" s9 V- ]ARM硬件上不支持除法指令,当代码中出现除法运算时,ARM编译器会调用C库函数(有符号的除法调用_rt_sdiv,无符号的调用_rt_udiv),来实现除法操作。有许多不同类型的除法程序来适应不同的除数和被除数。& A* W% f: s! g* s. e
& R7 `0 y6 h) N( w% k2 u1 T6 p; C: B2 q% K5 n, z5 q* O, e' s
总结:* g) d. B- T5 I( l5 p/ H$ `+ y- L- Q7 {9 z" ]# [, [2 }! c
1) 尽可能避免使用除法。对环形缓冲区的处理可以不用除法。
* l' B& Q# G* }+ Y+ J2) 如果不能避免除法运算,那么尽可能考虑使用除法程序同时产生商n/d和余数n%d的好处。- Q& h7 g3 b4 A$ o J0 `: o
3) 对于重复对同一除数d的除法,预先计算好s=(2k-1)/d。可用乘以s的2k位乘法来代替除以d的k位无符号整数除法。- G9 { l* W# J, @
4)使用2的整数次幂作除数。当2的整数次幂做除数时,编译器会自动将除法运算转换成移位运算。所以在编写程序算法时,尽量使用2的整数次幂做除数。
q. Z! V; ^5 ~; t# M, i6 x5)求余运算。可以将一些典型的求余运算进行转换,以避免在程序中使用除法运算。如:
) \) B5 ^) N5 Q8 v4 U- ) T4 G9 s/ H% C1 g7 H* b* m& v! ~
/ ?1 ]& Q# b8 z2 s; p, W
$ o; t7 S! ?* a# o! P( H( q+ ] uint counter1(uint count){return (++count%60);}转换成:uint counter2(uint count){if (++count >=60) count=0;return (count);}% ^" D8 [/ f% }% `: w. W3 w. v H2 v z y2 j& A9 D5 H* J
: [: b7 ?: ~2 G/ w, f7 v! k十、浮点运算
4 p, g- i- B9 h+ t& r大多数ARM处理器硬件上并不支持浮点运算。这样在一个对价格敏感的嵌入式应用系统中,可节省空间和降低功耗。除了硬件向量浮点累加器VFP和ARM7500FE上的浮点累加器FPA外,C编译器必须在软件上提供浮点支持。
/ N. s( X: E7 J9 K& F: _3 V& E7 I5 q7 F& S0 t a, Q6 a7 H# \) z4 y. Z
十一、内联函数和内嵌汇编
2 {& Q! m- k' K4 [$ o7 @高效地调用函数,使用内联函数可以完全去除函数调用的开销,另外许多编译器允许在C源程序中使用内嵌汇编。使用包含汇编的内嵌函数,可以使编译器支持通常不能有效使用的ARM指令和优化方法。
# J8 [9 [3 l; `* d& a( J% W' l! e8 R& v8 P8 Q9 N* C
t3 P' G* m3 ]( A内联函数和内嵌汇编最大的好处是,可以实现一些在C语言部分中通常难以完成的操作。使用内联函数要比使用#define宏定义更好,因为后者不检查函数参数和返回值的类型。- P1 b1 \" M5 p$ a# F+ a5 f8 f# t
|
|