C语言编译的优化等级应该选哪个?O0、O1、O2还是O3

在使用IDE开发STM32程序时,IDE一般都会提供优化等级设置的选项,例如下图中KEIL软件优化等级的设置。

在这里插入图片描述
从上图中也可以看出,设置不同的优化等级,实际上是修改了编译器的编译参数。这个编译器是由ARM公司提供的C/C++编译器armclang或者armcc。编译器提供了不同的优化等级,能够优化由用户代码生成的目标代码。这里的优化主要针对4个方面:

  • 代码的运行效率
  • 生成的目标代码的体积
  • 调试信息是否完整准确
  • 生成目标代码的构建时间长短

针对以上4点,编译器提供了不同的参数,以满足不同的需求,见下表所示
优化目标与参数
从上表可知,在keil软件中能够选择的O0、O1、O2和O3,针对的是目标代码的运行效率,等级越高,对目标代码的优化就越多,运行效率就越高。不过在提高了运行效率的同时,会使其它几个方面的变差,比如会增加目标代码的体积、调试信息失真,以及需要更长的编译时间。

下面针对O0、O1、O2和O3这几个等级,分别介绍它们的特点和区别。(部分涉及到编译优化方法的名词会在最后的附录中进行解释,遇到不理解的名词可以查看文章最后的附录)

O0等级

该等级为最小的优化等级,关闭了大多数的优化措施。这种情况生成的固件和用户编写的代码几乎是对应的,因此用户在调试代码的时候,能够根据源代码更快的定位到编译后的位置。

在O0这个优化等级下,不会对代码进行优化,不会删除用户的死代码(dead code,死代码指的是编写了但是没有用到的代码,也包括不起作用的代码),不会删除没有使用的变量,能够读到各个函数完整的栈信息(不理解这句话的,可以向下看O1等级的第4条,对比一下就理解了)。

O1等级

该等级实现受限的优化。在这个模式下会对用户代码进行优化,同时尽量不影响用户的调试信息。与O0的不同之处如下:

  1. 调试的断点不能设置到死代码上,因为死代码会被优化掉;
  2. 有的临时变量,虽然还在其作用域中,但是也会被清除,比如这个变量在后面没有再用到,而且其所在的栈的位置被调用的子函数占用了,那么在返回时这个变量可能不会恢复。
  3. 死代码也就是没有作用的代码会被移除,比如没有被调用的函数,或者这个函数对外部没有任何影响的(no side-effects ),以下面这个代码为例,这个函数用到的全是临时变量,而且也没有返回值的,那么这个函数就不会对外接产生任何影响,也就认为没有任何作用,就会被优化掉。
void add_fake(){
	int a=1,b=2;
	int c = a+b;
}
  1. 由于尾调用的存在,回溯(backtrace)可能无法提供从源代码阅读中期望的函数调用栈。尾调用如下面的代码中语句return add(a,b),这个语句就是尾调用,尾调用是一种在函数末尾直接调用另一个函数的优化技术,它的特点是当前函数在调用另一个函数后不会保留任何上下文或返回地址,而是直接跳转到被调用函数的执行。在支持尾调用优化的编程语言或环境中,这种优化可以减少栈帧的数量,提高程序的执行效率。然而,正因为尾调用优化的存在,当程序出现错误或需要回溯时,传统的回溯技术可能无法准确地展示函数调用的完整栈。这是因为尾调用不会创建新的栈帧,而是重用了当前栈帧,这可能导致回溯结果中缺少某些函数调用的信息,或者函数的调用顺序看起来与源代码中的顺序不符。
void test(){
	int a=1,b=2;
	return add(a,b);
}

在这个优化等级下,进行的优化比较少,主要是针对死代码和没有用的变量进行了优化,生成的目标代码的结构和源代码区别不大,因此对用户的调试没多大影响。而且因为O1删除了死代码,因此其生成的代码体积要小于O0等级的。

O2等级

高等优化。这个等级优化后的目标代码和源代码不会一一对应,这是因为使用了类似于循环展开、程序内联、常量折叠、公共子表达式消除、死代码消除、向量化等编译优化措施,优化了生成的代码结构。因此在调试的时候可能会发现无法很好的定位到源代码对应的目标代码的位置,不利于对源代码的调试。O2等级使用了O1等级的所有优化方法,同时还使用了:

  1. 由于源代码中的多个位置可能映射到目标代码中的同一个点,以及编译器可能进行的更激进的指令调度,源代码到目标代码的映射可能是多对一的关系。这意味着从目标代码回溯到源代码可能不是直接的或明确的。
  2. 编译器在优化过程中进行的指令调度,如果允许跨越序列点,可能会导致在特定点报告的变量值与直接从源代码中预期的值之间存在不匹配。因此在分析编译后的代码或调试程序时,需要特别注意编译器优化可能引入的这种复杂性。
  3. 编译器会自动内联函数。

O3等级

这个等级会对用户代码进行最大的优化。这个等级下的优化除了包含O2等级的优化,还包含:

  1. 通过包括循环展开在内的高级标量优化,可以在较小代码尺寸代价下获得显著的性能提升。通过循环展开和其他高级标量优化,通常可以获得显著的性能提升,因为减少了循环控制和分支预测的开销,并可能提高了数据访问的局部性。但也会增加编译生成目标代码的构建时间。这要求开发者在权衡性能提升和构建时间之间做出决策。
  2. 更激进的内联和自动内联。编译器会更加积极地选择更多的函数进行内联,而不仅仅是那些显然可以带来性能提升的函数。这种更激进的做法可能会增加代码的大小,但通常可以带来更好的性能。编译器通常会基于一系列因素(如函数大小、调用频率、函数的复杂度等)来决定是否要进行内联。

编译器对代码的优化,会使目标代码的结构和源代码的结构相差很大,因此使得用户在通过源代码进行调试的时候,有更糟糕的体验。因此ARM公司不推荐在这种优化等级下使用调试功能。

总结

从上面的内容可以知道,更高的优化等级会使用更多的编译优化技术,对用户的源代码进行优化,从而使得生成的目标代码的结构与源代码的结构区别很大,这样就不利于用户对代码的调试。

因此在前期编写代码时,最好将优化等级设置为O0和O1,这样能够更方便的进行调试,相比于O0,ARM公司更推荐使用O1等级,因为O1等级会进行一部分优化而且对调试影响不大。而后期要交付产品的时候,为了追求代码的运行效率,可以将优化等级调整到O2和O3。

附录-常用的编译优化手段

在上面的文章中,提到了很多的编译优化方法,平时没接触处过编译的知识,所以会不好理解,下面对上面提到的优化方法,以及一些其它的常用优化方法进行列举和解释。

  1. 尾调用是一种在函数末尾直接调用另一个函数的优化技术,它的特点是当前函数在调用另一个函数后不会保留任何上下文或返回地址,而是直接跳转到被调用函数的执行。在支持尾调用优化的编程语言或环境中,这种优化可以减少栈帧的数量,提高程序的执行效率。然而,正因为尾调用优化的存在,当程序出现错误或需要回溯时,传统的回溯技术可能无法准确地展示函数调用的完整栈。这是因为尾调用不会创建新的栈帧,而是重用了当前栈帧,这可能导致回溯结果中缺少某些函数调用的信息,或者函数的调用顺序看起来与源代码中的顺序不符。
  2. 源代码到目标代码的映射:在编译过程中,源代码(即程序员编写的代码)会被转换成目标代码(即机器可以直接执行的代码)。这种转换通常是通过一系列步骤完成的,包括词法分析、语法分析、语义分析、优化和代码生成等。
  3. 多个源代码位置映射到一个目标代码点:这通常发生在编译器进行优化时。例如,某些在源代码中明显分开的操作可能在目标代码中合并成一个。或者,源代码中的某些变量或常量可能在编译时被优化或消除,导致它们在目标代码中不再有明显的对应。
  4. 指令调度是编译器优化技术的一种,其主要目的是重新排列指令以提高执行效率。更激进的指令调度可能意味着编译器会更大胆地重新排列或合并源代码中的指令,以生成更高效的目标代码。这同样可能导致源代码和目标代码之间的映射关系变得复杂。
  5. 指令调度允许跨越序列点:在编译过程中,指令调度是一种优化技术,用于重新安排程序中指令的执行顺序,以改善程序性能。序列点(sequence points)是程序中特定的点,在这些点上,所有之前的副作用(如变量值的改变)都必须完成,并且这些变化对后续代码可见。然而,在某些情况下,编译器可能会进行更激进的优化,允许指令跨越这些序列点进行调度。
  6. 可能导致变量值的不匹配:当指令调度跨越序列点时,这可能会导致在源代码中看起来应该按特定顺序执行的操作在实际的目标代码中不再保持这个顺序。因此,如果在某个特定点检查一个变量的值,可能会发现该值与从源代码直接阅读时所期望的值不匹配。
  7. 内联函数(Inline Function)是一种编译器优化技术,用于减少函数调用的开销。当一个函数被声明为内联时,编译器会尝试在调用该函数的地方直接插入(或“内联”)该函数的代码,而不是进行常规的函数调用。这可以消除函数调用的开销,包括参数传递、栈帧的创建和销毁等,从而可能提高程序的执行效率。当编译器“自动内联函数”时,它会自动决定哪些函数应该被内联,而不需要程序员显式地指定。这通常基于函数的尺寸、调用频率和其他一些因素。需要注意的是,过度使用内联可能会导致代码膨胀,从而可能增加指令缓存的未命中率和其他负面影响,所以编译器在内联函数时会进行权衡。同时,有些函数可能不适合内联,例如那些有循环或复杂控制流的函数。因此,尽管编译器可以自动内联函数,但程序员仍然需要了解内联的优缺点,并在必要时通过编译器选项或特定的代码标记来控制内联行为。
  8. 高级标量优化:标量优化是针对单个变量或数据项的优化,而不是针对向量或数组。高级标量优化通常涉及复杂的算法转换和代码变换,以改进程序的执行效率。
  9. 循环展开:循环展开是一种常用的优化技术,它通过减少循环次数并复制循环体中的代码,来减少循环的控制开销。例如,一个原本执行四次迭代的循环可以被展开成四份独立的代码,每份代码执行一次原循环体中的操作。
  10. 常量折叠:在编译时,如果编译器能够确定某个表达式的结果是一个常量,那么它会在编译时直接计算出这个常量值,并将其替换到代码中的相应位置。
  11. 常量传播:如果一个变量的值在编译时已知且是常量,编译器可能会用这个常量值替换掉所有对该变量的引用。
  12. 公共子表达式消除:如果一个表达式在程序中多次出现,并且每次出现时其值都没有变化,编译器会识别出这是一个公共子表达式,并只计算一次,存储结果,然后在需要的地方使用这个存储的结果。
  13. 死代码消除:编译器会检测代码中永远不会被执行的部分(死代码),并将其从最终的可执行文件中移除。
  14. 复写传播:编译器会识别出两个或多个变量实际上持有相同的值,并用一个变量来替换它们。
  15. 指令重排:编译器会重新排列指令的顺序,以减少数据依赖、提高缓存利用率或利用处理器的指令并行能力。
  16. 寄存器分配:编译器会智能地为变量分配寄存器,以减少内存访问次数,提高程序性能。
  17. 轮廓分析(Profile Guided Optimization):编译器使用程序运行时的轮廓信息(例如,哪些代码路径被频繁执行)来指导优化过程。这通常涉及到在程序运行时收集数据,并在后续的编译过程中使用这些数据。
  18. 类型优化:编译器可能会利用类型信息来优化代码,例如通过减少不必要的类型转换或使用特定的指令集来优化特定类型的数据处理。
  19. 向量化和并行化:编译器会尝试将代码转换为向量操作(即一次处理多个数据元素),并利用多核处理器进行并行处理,以提高计算密集型任务的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/588753.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

家里挂宗教画是否合适?

凡在我们这改名取名的客户,峰民都会建议他们挂一副这样或那样的画,这也是根据你命中的五行来给你助运的。而现在许多人都有自己的宗教信仰,有些人会在家供奉佛像,有些人甚至会在家里挂上宗教画,亦同样产生五行效应&…

持续更新|UNIAPP适配APP遇到的问题以及解决方案

在使用UNIAPP开发APP的时候遇到的一些奇奇怪怪问题记录 组件样式丢失 问题:组件引入界面中,在小程序和H5环境下样式正常,而在APP中却出现高度异常问题 解决:增加view标签将组件包裹起来即可正常显示 解决前: 解决后…

全栈开发之路——前端篇(2)文件、组件与setup引入

全栈开发一条龙——前端篇 第一篇:框架确定、ide设置与项目创建 本文系该系列第二篇,主要将介绍各个文件的意义、组件结构与导入以及setup的引入。 目录 一、src外文件介绍.gitignore为git忽略文件env.d.ts用于识别其他文件index.htmljson文件vite.confi…

企业信创中间件落地的 9 个实践经验:从选型测试到使用和运维

背景 随着各个行业对IT系统建设自主可控要求的提升,各个企业对信创中间件的使用范围也越来越广、深度越来越深。与此同时,信创中间件涉及的产品和种类也是多种多样,对于一个企业而言要从哪些维度来评估信创中间件,从而选择适合自…

规控不分家,实际岗位职责是如何划分的

1. 实践和演练 2. 自动驾驶技术分类 3. 自动驾驶关键技术 4. 自动驾驶架构 5. 感知perception

SpringDI方式及Redis应用场景的分享

1、为什么Spring和IDEA 都不推荐使用 Autowired 注解 大家在使用IDEA开发的时候有没有注意到过一个提示,在字段上使用Spring的依赖注入注解Autowired后会出现如下警告Field injection is not recommended (字段注入是不被推荐的);但是使用Resource却不会…

【linux学习指南】linux指令与实践文件编写

文章目录 📝前言🌠 linux目录结构🌉linux命令介绍 🌠pwd命令🌉mkdir指令(重要) 🌠cd 指令🌉touch指令 🌠rmdir指令 && rm 指令(重要&…

串的介绍与类型定义

串的定义 相关术语 应用 串类型定义 顺序存储实现 为了方便操作,数组下标从1开始存储 链式存储实现

【Leetcode】二叉树基础题思路

🔥个人主页:Quitecoder 🔥专栏:Leetcode刷题 目录 1.单值二叉树2.相同的树3.对称二叉树4.另一棵树的子树 1.单值二叉树 题目链接:965.单值二叉树 题目描述: 单值二叉树是所有节点的值都相同的二叉树。实现…

MySQL LIKE通配符(%,_)及escape实例讲解

LIKE操作符常用于模式匹配查询数据。以正确的方式使用LIKE运算符对于提高查询性能至关重要。 LIKE操作符允许您从基于指定的模式选择表中的数据。因此,LIKE操作符经常用于SELECT语句的WHERE子句中。 MySQL提供了两个通配符与LIKE操作符一起使用:百分比…

LeetCode 98.验证二叉搜索树

题目描述 给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下: 节点的左 子树 只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 示例 1&#xff…

移植 SquareLine 导出的 UI 源码到 HMI-Board

目录 准备工具创建 HMI 工程设计 UIUI 移植板级验证更多内容 HMI-Board 为 RT-Thread 联合瑞萨推出的高性价比图形评估套件,取代传统的 HMI 主控板 硬件,一套硬件即可实现 HMI IoT 控制 的全套能力。依托于瑞萨高性能芯片 RA6M3 及 RT-Thread 软件生态…

leetcode870.优势洗牌

题目描述: 给定两个长度相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。 示例一: 输入&#xff…

nginx--平滑升级

失败了,等我拍好错继续更新 命令 选项说明 帮助: -? -h 使用指定的配置文件: -c 指定配置指令:-g 指定运行目录:-p 测试配置文件是否有语法错误:-t -T 打印nginx的版本信息、编译信息等:-v -V 发送信号: -s 示例: nginx -s reload 信号说明 立刻停止服务:stop,相…

笔记:编写程序,绘制一个展示支付宝月账单报告的饼图,

文章目录 前言一、饼图是什么?二、分析题目三、编写代码总结 前言 编写程序,绘制一个展示支付宝月账单报告的饼图,实现过程如下: (1) 导入 matplotlib.pyplot 模块; (2)…

主成分分析在R语言中的简单应用:使用mvstats包

在数据科学领域,主成分分析(PCA)是一种广泛使用的技术,主要用于数据降维和探索性数据分析。PCA可以帮助我们发现数据中的模式,减少数据集的复杂性,同时保持数据中最重要的特征。本文将介绍如何在R语言中使用…

【经典算法】LeetCode112. 路径总和(Java/C/Python3/Go实现含注释说明,Easy)

作者主页: 🔗进朱者赤的博客 精选专栏:🔗经典算法 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名) ❤️觉得文章还…

实时监控RTSP视频流并通过YOLOv5-seg进行智能分析处理

在完成RTSP推流之后,尝试通过开发板接收的视频流数据进行目标检测,编写了一个shell脚本实现该功能,关于视频推流和rknn模型的部署请看之前的内容或者参考官方的文档。 #!/bin/bash # 设置脚本使用的shell解释器为bashSEGMENT_DIR"./seg…

OceanBase开发者大会实录-陈文光:AI时代需要怎样的数据处理技术?

本文来自2024 OceanBase开发者大会,清华大学教授、蚂蚁技术研究院院长陈文光的演讲实录—《AI 时代的数据处理技术》。完整视频回看,请点击这里>> 大家好,我是清华大学、蚂蚁技术研究院陈文光,今天为大家带来《AI 时…

JUC线程

进程和线程: 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。 线程(英语:thread)是操作系统能够进行运算调度…
最新文章