C编译器生成执行速度更快的ARM代码与性能相关的关键点

以某种风格编写 C 程序有助于 C 编译器生成更快的 ARM 代码。以下是与性能相关的一些关键点:

1、局部变量、函数参数和返回值使用int类型。这避免了类型转换并有效地使用了 ARM 的 32 位数据操作指令。

2、循环体最有效的形式是倒计时到零的do-while循环。

3、展开重要循环以减少循环开销。

4、不要依赖编译器来优化重复的内存访问。指针别名可防止编译器进行这种优化。

5、尽可能将函数参数的个数限制为4个。如果函数参数存储在寄存器中,那么函数调用会快很多。

6、按元素大小的升序排列结构,尤其是在拇指模式下编译时。

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 类型效率更高。

总结:

1)对于存储在寄存器中的局部变量,除了8位或16位算术取模操作外,尽量不要使用char和short类型,而是使用或 int类型。除法时使用无符号数更快。

2)对于存储在主存中的数组和全局变量,在满足数据大小的前提下,尽量使用小尺寸的数据类型,这样可以节省存储空间。ARMv4 架构可以高效地加载和存储所有宽度的数据,并且可以使用递增的数组指针来高效地访问数组。对于短类型数组,请避免使用从数组基地址开始的偏移量,因为 LDRH 指令不支持偏移量寻址。

3)当读取数组或全局变量并赋值给不同类型的局部变量时,或者将局部变量写入不同类型的数组或全局变量时,需要进行显式的数据类型转换。这种转换允许编译器显式和快速地处理内存中较窄数据类型的扩展,并将它们分配给寄存器中的较宽类型。

4)由于隐式或显式数据类型转换通常会有额外的指令周期开销,因此在表达式中应尽可能避免。加载和存储指令一般不会产生额外的转换开销,因为加载和存储指令会自动完成数据类型转换。

5)函数参数和返回值尽量避免使用char和short类型。即使参数范围很小,也应该使用 int 类型来防止编译器进行不必要的类型转换。

C循环结构

在 ARM 上,一个循环实际上只需要 2 条指令:

执行循环减法计数并设置结果条件标志的减法指令;条件分支指令。这里的关键是循环的终止条件应该是向下计数到零,而不是计数到某个特定限制。由于向下计数结构存储在条件标志中,因此可以省略比较零指令。由于 i 不作为数组的下标索引,所以倒数没有问题。

总之,无论有符号循环计数值如何,都应该使用 i !=0 作为循环的结束条件。对于有符号数 i,这比使用条件 i>0 少一条指令。

总结:

1) 使用倒数到零的循环结构,这样编译器就不需要分配寄存器来保存循环终止值,并且可以省略比较零指令。

2) 使用无符号循环计数值,循环继续条件 i!=0 而不是 i>0,这确保循环开销只有两条指令。

3) 如果事先知道循环体至少会执行一次,那么最好使用do-while循环而不是for循环,这样就省去了编译器检查循环次数是否为零。

4) 展开重要的循环体可以减少循环开销,但不要过度展开。如果循环开销只占整个程序的一小部分,循环展开将增加代码大小并降低缓存性能。

5) 尽量使数组的大小为 4 或 8 的倍数,这样就可以轻松展开循环,使用 2、4、8 等选项,而不必担心剩余的数组元素。

寄存器分配

高效的寄存器分配:应尽量将函数内循环使用的局部变量数量限制在不超过 12 个,以便编译器可以将这些变量分配给 ARM 寄存器。

函数调用

注册规则:具有 4 个或更少参数的函数比具有 4 个以上参数的函数执行效率更高。对于参数少于4个的函数,编译器可以通过寄存器传递所有参数;对于参数超过4个的函数,函数调用者和被调用者必须通过访问堆栈来传递一些参数。

如果函数很小并且使用的寄存器很少,还有其他方法可以减少函数调用的开销。可以将调用函数和被调用函数放在同一个C文件中,这样编译器就可以知道被调用函数生成的代码,并对调用函数进行一些优化。

总结:

1) 尽量限制函数的参数不超过4个,这样函数调用会更有效率。也可以将多个相关参数组织在一个结构中,并传递结构指针而不是多个参数。

2) 将较小的被调用函数和调用函数放在同一个源文件中,先定义,再调用,编译器可以优化函数调用或者内联较小的函数。

3) 可以使用关键字内联对性能有很大影响的重要函数。

指针别名

定义:当两个指针指向同一个地址对象时,这两个指针称为该对象的别名。如果您写入其中一个指针,它将影响从另一个指针的读取。在一个函数中,编译器通常不知道哪些指针是别名的,哪些不是;或者哪些指针是别名的,哪些不是。

避免指针别名:

1) 与其依赖编译器来消除包含内存访问的公共子表达式,不如创建一个新的局部变量来保存这个表达式的值,这样可以确保这个表达式只被计算一次;

2) 避免使用局部变量的地址,否则访问该变量的效率会降低。

结构安排

在 ARM 上使用结构体时需要考虑两个问题:结构体地址边界对齐和结构体的整体大小。

获得高效结构的原则:

1) 将所有 8 位元素排列在结构体前面;

2) 以这种方式排列 16 位、32 位和 64 位元素;

3) 将所有数组和较大的元素排列在结构的末尾;

4) 对于一条指令,如果结构太大而无法访问所有元素arm汇编指令中的变量,则将元素组织成子结构。编译器可以维护指向各个子结构的指针。

总结:

结构的元素要按照元素的大小排列,最小的元素在开头,最大的元素在结尾;避免使用大的结构arm汇编指令中的变量,并用小的层次结构代替它;为了提高可移植性,在API的结构中人为地添加了填充位,使得结构的排列不依赖于编译器;在 API 的结构中谨慎使用枚举类型。枚举类型的大小取决于编译器。

位域

预防措施:

1) 避免使用位域,使用#或枚举来定义掩码位;

2) 使用整数逻辑 AND、OR、XOR 和掩码测试、取反和设置位域。这些操作编译效率高,可以同时测试、否定和设置多个位域。

边界未对齐的数据和字节排列(大/小端)

边界未对齐数据和字节排列这两个问题会使内存访问和移植问题复杂化。必须考虑数组指针是否在边界上对齐,以及ARM配置是大端(big-)还是小端(-)内存系统。

总结:

1) 尽量避免使用边界不对齐的数据;

2) 使用 char * 类型指向任意字节边界上的数据。通过读取字节访问数据,通过逻辑操作组合数据,使代码不依赖边界对齐或ARM的字节排列配置;

3) 为了快速访问边界上未对齐的结构,可以根据指针边界和处理器的字节顺序编写不同的程序变体。

分配

ARM 硬件不支持除法指令。当代码中出现除法运算时,ARM编译器会调用C库函数(有符号除法调用、无符号调用)来实现除法运算。有许多不同类型的除法例程来适应不同的除数和除数。

总结:

1) 尽可能避免分裂。环形缓冲区的处理可以不进行分割。

2) 如果无法避免除法,请考虑使用除法例程尽可能同时产生商 n/d 和余数 n%d 的好处。

3) 对于同一个除数 d 的重复除法,预先计算 s=(2k-1)/d。k 位无符号整数除以 d 可以用 2k 位乘以 s 代替。

4)使用 2 的幂作为除数。当使用 2 的整数幂作为除数时,编译器会自动将除法运算转换为移位运算。因此,在编写程序算法时,尽量使用 2 的整数幂作为除数。

5)剩余操作。可以转换一些典型的余数运算以避免程序中的除法运算。喜欢:

图片[1]-C编译器生成执行速度更快的ARM代码与性能相关的关键点-4747i站长资讯

浮点运算

大多数 ARM 处理器硬件不支持浮点运算。这可以节省空间并降低对价格敏感的嵌入式应用系统的功耗。除了硬件向量浮点累加器 VFP 和浮点累加器 FPA,C 编译器还必须在软件中提供浮点支持。

内联函数和内联汇编

高效调用函数,使用内联函数可以完全去掉函数调用的开销,而且很多编译器允许在C源程序中使用内联汇编。使用包含汇编的内联函数允许编译器支持通常无法有效使用的 ARM 指令和优化。

内联函数和内联汇编最大的优点是可以实现一些在C语言部分通常很难做到的操作。使用内联函数比使用#macro 定义要好,因为后者不检查函数参数和返回值的类型。

文章来源:http://baijiahao.baidu.com/s?id=1675622557279311458&wfr=spider&for=pc

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享