如何在ARM下进行高效的C编程?

article/2025/11/5 16:14:08

通过一定的风格来编写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类型也会更有效。     

       

总结:

              

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.详解嵌入式Linux工程师的成长经历

2.单片机5V转3.3V电平,有19种方法技巧供参考!

3.嵌入式WiFi与普通WiFi有什么不同?

4.Linux开发者都应该知道的知识要点~

5.研究完比特币代码,发现了一个惊人秘密!

6.开发者避坑须知:2018 年最不值得学习的五门编程语言

免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。


http://chatgpt.dhexx.cn/article/4FQkAuw1.shtml

相关文章

## ARM基础编程实验

ARM基础编程实验 作者: Saint 掘金:https://juejin.im/user/5aa1f89b6fb9a028bb18966a 微博:https://weibo.com/5458277467/profile?topnav1&wvr6&is_all1 GitHub:github.com/saint-000 CSDN: https://me.csdn.net/qq_4…

ARM汇编编程基础

(一) -- ARM CPU寄存器 本系列文章节选自本人所著《深入浅出嵌入式底层软件开发》。 本系列文章,所需代码请从以下地址下载: http://download.csdn.net/download/scyangzhu/4602585 ARM的汇编编程,本质上就是针对CP&#…

搭建Keil编程环境,ARM汇编语言编程入门实践

多看多学 一、安装及配置环境01 型号分类及缩写02 资料共享03 安装器件支持包04 Keil简单设置 二、简单程序编译01 新建工程02 配置环境03 添加源文件04 设置仿真器模式05 编译调试06 分析Hex文件 三、中值滤波及程序设计01 中值滤波02 流程框图03 程序设计 四、总结参考资料 一…

ARM编程环境搭建教程

随着移动互联网的繁荣发展,物联网人工智能的兴起,嵌入式开发成为了越来越多IT人必须学习的内容,而在嵌入式芯片领域,ARM具有无可撼动的市场占有率,所以说,学习嵌入式不学ARM,不如回家卖红薯。 接…

嵌入式ARM设计编程(四) ARM启动过程控制

文章和代码已归档至【Github仓库:hardware-tutorial】,需要的朋友们自取。或者公众号【AIShareLab】回复 嵌入式 也可获取。 一、实验目的 (1) 掌握建立基本完整的ARM 工程,包含启动代码,C语言程序等&…

《嵌入式基础》实验三 ARM编程模型和ARM指令

零、前言 本人不擅长写汇编相关的东西,所以以下内容也是不断摸索(百度 ) 整出来的,和linux的实验报告的质量相比较低。 一、 实验目的 掌握ARM微处理器的汇编指令的使用方法。掌握使用 LDM/STM,B,BL 等指…

嵌入式ARM设计编程(一) 简单数据搬移

文章和代码已归档至【Github仓库:hardware-tutorial】,需要的朋友们自取。或者公众号【AIShareLab】回复 嵌入式 也可获取。 一、实验目的 熟悉实验开发环境,掌握简单ARM汇编指令的使用方法。 二、实验环境 硬件:PC机 软件&am…

ARM汇编语言编程入门实践

文章目录 一、keil的安装二、安装stm32 pack三、keil 的简单设置四、基于STM32汇编程序的编写4.1 新建工程4.2 新建test.s文件4.3 编译程序4.4 分析HEX文件 五、总结六、参考资料 一、keil的安装 双击打开mdk_510.exe应用程序文件,点击Next>>。勾选I agree……

ARM体系结构与编程模型总结

ARM体系结构与编程模型 ARM体系结构 一、ARM处理器简介及RISC特点 ARM处理器简介 ARM(Advanced RISC Machines)是一个32位RISC(精简指令集)处理器架构,ARM处理器则是ARM架构下的微处理器。ARM处理器广泛的使用在许多…

数学建模-层次分析法(评价模型)

层次分析法概述 层次分析法的步骤和方法 1. 建立层次结构模型 2. 构造判断(成对比较)矩阵 3. 层次单排序及其一致性检验 4. 层次总排序及其一致性检验 总结 应用层次分析法的注意事项 举例 层次分析法代码实现 disp(输入判断矩阵C) Cinput(C); %输入矩阵 [n,n] size(C);%…

数学建模-层次分析模型

层次分析法的基本原理与步骤 人们在进行社会的、经济的以及科学管理领域问题的系统分析中,面临的常常是一个由相互关联、相互制约的众多因素构成的复杂而往往缺少定量数据的系统。层次分析法为这类问题的决策和排序提供了一种新的、简洁而实用的建模方法。 运用层次…

数学建模-层次分析法

个人主页: 个人主页 系列专栏: 数学建模 目录 前言 一、引入 二、层析分析法 1.思想介绍 2.判断矩阵 3.一致矩阵 4.一致性检验 5.一致性检验步骤(两张图片告诉你) 6.计算权重 一致矩阵计算权重: 判断矩阵计…

数学建模--层次分析法

层次分析法的求解步骤 1.建立层次结构模型 模型分为三层。分别为最高层(决策问题最终要解决什么,即决策的目的)、中间层(考虑的因素,决策的准则。比如买衣服要考虑价格、尺寸、款式等因素)和最低层&#…

层次分析法模型(数学建模学习)

本系列参考清风老师的数学建模课程 层次分析法模型 一、模型介绍 (一)模型引入 对于方案选择类问题,评价类问题采用层次分析法(The ayalytic hierarchy process / AHP)模型进行评分,之后评分高的就是最佳…

数据库常用数据模型1(层次模型)

1 数据模型分为两类:一类是概念模型(信息模型),第二类是逻辑模型(数据建模)和物理模型(对数据最底层的抽象)。 2 常用的逻辑模型:层次模型、网状模型、关系模型、面向对…

计算机网络层次模型

计算机网络层次模型 OSI 7层协议 物理层:在局部局域网上传送数据帧(Data Frame),它负责管理电脑通信设备和网络媒体之间的互通。数据链路层:在两个网络实体之间提供数据链路连接的创建、维持和释放管理。构成数据链路…

批判马斯洛需求层次模型

再次批判马斯洛需求层次模型,有啥缺陷? 趣讲大白话:文化不同,心理不同 【趣讲信息科技182期】 **************************** 每个民族的文化心理结构都不一样 常常低估文化对人的影响 有一门心理学分支,文化心理学 专…

网络层次模型及各层对应协议

一、OSI七层模型 OSI七层协议模型主要是:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数…

层次分析法模型

层次分析法 文章目录 层次分析法概述层次分析法简介层次分析法典型应用层次分析法基本原理 层次分析法的步骤和方法建立层次结构模型构造判断矩(成对比较)阵层次单排序及其一致性检验一致性检验正互反阵最大特征根和特征向量的简化计算 层次总排序及其一…

层次分析法模型讲解

一、解决评价类问题 二、资料来源 三、例题 1.准备 2.权重表格 3.分而治之解决权重弊端 4.层次分析法 根据判断矩阵求权重,下面对各个指标进行填写判断矩阵: bug分析: 如果小明填写的不是5而是1,这里发现:苏杭>北戴…