打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
干货!C 代码优化策略总结
userphoto

2023.02.25 湖南

关注

一、前言

C++的性能真的比C语言的要差么?人们通常所持的C++性能差的观点是不正确的。确实,在一般情况下,如果把C语言和看起来与C语言相同的C++版本相比,前者通常要快一些。但同时两种语言在表面上的相似性通常是基于它们的数据处理功能,而不是它们的正确性、健壮性和易维护性。我们的观点是如果让C语言程序在上述方面达到C++程序的级别,则速度差别就会消失,甚至可能是C++版本的程序更快。

C++不是天生就较慢或较快,这两者都是有可能的,关键要看怎样使用它以及想从它那里得到什么。这与如何使用C++有关系:运用得当的话,C++不仅可以让软件系统具备可接受的性能,甚至还可以获得出众的性能

二、软件低效的根源

图片来自《C++性能优化指南》

● 语言结构

C++在其原型C中增加了新能力和灵活性。这些新增的益处(eg:新特性、新语法)并不是白来的。某些C++语言结构可能会以产生开销作为代价。

● 系统体系结构

不考虑系统体系结构开发软件也很容易。然而要达到高性能,就不能无视体系结构的种种问题,因为它们在相当大的程度上影响到性能。

当提到性能时,我们必须记住以下几点:

● 内存不是无限大的。虚拟内存系统使得内存看起来是无限的,而事实上并非如此。

● 内存访问开销不是均衡的。对缓存、主内存和磁盘的访问开销不在同一个数量级之上。

● 我们的程序没有专用的CPU,只能间歇地获得一个时间片。

● 在一台单处理器的计算机上,并行的线程并不是真正地并行执行,它们是轮询的。

● 库

对库的选择与使用也会影响性能。为了性能提升,即便库中已存在某种特殊功能的函数,您还是可以选择自己去编写一个版本。

设计库时人们常常将灵活性和可重用性作为指导思想。一般来说,灵活性和可重用性与性能之间存在一种折中。如果认为某段代码的性能比其灵活性和可重用性都重要,那么使用自己的实现来替代库提供的功能就是合理的。由于不同的程序有其各自特定的需求,所以很难设计出一个能够对任何人、在任何地点和时间都提供完美实现的库。

编译器优化

绝大多数编译器能够完成许多“消除计算冗余”的优化,但是不同编译器的优化方式和优化效果不一样,所以不能指望某一个特定的编译器进行特定优化。为了最大程度地控制程序,您必须自己动手解决编码问题。

三、性能优化观点

代码优化不会导致项目的业务异常或项目延期,不能因编程恶习或逃避做优化代码分析而不做优化工作。

个人建议:

项目应该提前整理好编码checklist和编码规范,包含常用的编码注意点和编码建议,这样可以让开发人员从一开始编码就参考着编写高质量规范的代码。

需求分析和设计时,也要考虑内存占用、性能等各种非功能性需求。

编码完成,项目进入集成测试阶段,进行性能测试与代码优化和代码质量加强。

  1. 应从一开始就写更加高效的代码,编写普通代码和编写高效代码耗时差异是不大的。
  2. 优化不要过于教条,所有的软件开发的最佳实践都可以参考,但是不能因为其他项目中用了哪个算法或某个数据结构,就在新项目中也使用。需要根据实际分析出的问题点进行综合考虑。
  3. 软件开发世界中,存在大量“优化是不重要的,可以通过堆硬件提升性能”的错误认知。如今多核处理器的性能不断强大,但是单个核心的性能增长却非常缓慢,甚至有时还有所下降。另外还要考虑到不同系统的兼容和迁移,所以,唯有优化可以让程序永远保持活力
  4. 快速迭代的项目,性能优化可能需要结合多个或多次的优化点一起进行改善;
  5. 及时同步最新版本,可能新版本代码中这个性能问题已经不存在,也可能存在更优先的性能问题。
  6. 性能特质倾向于高度的非直觉性,不要期望你每一单优化都有效果。

四、性能优化原则

  1. 性能优化点要测量,不要猜。需要根据实际分析出的问题点进行综合考虑,不能仅仅因为猜测,或者大家都觉得那个方式快就一定用哪个方法。
  2. 并行系统中,核数并非越多越好,到达一定数量后系统整体性能提升有限
  3. 帕累托法则(二八原则)

80%的执行时间花在大约20%代码身上;

80%的内存被大约20%的代码使用;

80%的维护成本花在20%的代码上面;

程序中只有 20% 的代码的性能是很重要的。因此,试图修改程序中的每条语句去改善程序性能没有必要,也不会有作用。要会去定位性能热点或性能瓶颈。

五、性能测量工具

把性能问题进行量化,比如XX吞吐量、XX并发量、RTT数据是XX毫秒等。然后选择合适的性能测试工具进行测试。

性能问题只有量化后才方便进行对比分析。要不然只是说“性能慢”没有一个准确的判断标准,也没有一个准确的测试工具,是没法进行分析和优化验证的。

图片来自网络

六、不同开发过程的性能优化点

6.1 设计

针对非功能性需求进行性能问题分析与定义,并进行对应的算法与数据结构设计,并对可能出现的性能问题、验证方法等做好设计。

6.2 实现

①编码:

1.考虑预先计算( 编译期编程:模板 、constexpr等)

2.考虑延迟计算( info log, copy-on-write )

3.考虑批量计算

4.尽量减少函数参数

②并发:

1.避免进程/线程间上下文切换

2.缩小临界资源范围

3.方法线程数量尽量不多于核数

6.3 编译

编译优化是成本收益比最好的优化手段。

1.选择更好的编译器及编译器版本。使用合适的编译选项。

2.Release考虑用反馈式编译

3.删除没必要的虚函数与函数指针

4.考虑关闭异常处理。比如try catch

5.考虑关闭RTT监测

6.考虑使用编译时多态替换运行时多态

七、关键优化点介绍

7.1 用好的编译器并用好编译器

每种编译器为 C++ 语句生成的机器码都有差别。它们所看到的优化机会是不同的,会为相同的源代码产生不同的可执行文件。如果打算为代码做出最后一丁点性能提升,那么你可以尝试一下各种不同的编译器,看看是否有一种编译器会为你产生更快的可执行文件。

C++有众多编译器,不同的编译器优化效果不一样;

每个编译器的不同版本,优化效果也不一样;

感兴趣的同学可以在一些在线编译器网站上查看,比如“https://gcc.godbolt.org/”网站展示的编译器有ZIG C++、ICX、ICC、GCC、CLANG、NVC++、MSVC、POWER等,就不一一列举了。大家平时比较常用的可能都是gcc。不管使用哪一种编译器,在做代码优化时,最好在官网下载对应的编译器说明文档,具体查看里面的优化选项细节。

关于如何选择 C++ 编译器的一条最重要的建议,是使用支持 C++11 的编译器。 C++11 实现了右值引用(rvalue reference)和移动语义(move semantics),可以省去许多在以前的C++ 版本中无法避免的复制操作

要用好编译器,是否打开了合适的编译选项。例如,检查是否打开了编译器的优化选项,比如-o1 、-o2 、-o3 、去掉-g等。

-O编译选项说明:

O0选项不进行任何优化,在这种情况下,编译器尽量的缩短编译消耗(时间,空间),此时,debug会产出和程序预期的结果。当程序运行被断点打断,此时程序内的各种声明是独立的,我们可以任意的给变量赋值,或者在函数体内把程序计数器指到其他语句,以及从源程序中 精确地获取你期待的结果.

O1优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化。

O2会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。

O3在O2的基础上进行更多的优化,例如使用伪寄存器网络,普通函数的内联,以及针对循环的更多优化。

Os主要是对代码大小的优化,我们基本不用做更多的关心。 通常各种优化都会打乱程序的结构,让调试工作变得无从着手。并且会打乱执行顺序,依赖内存操作顺序的程序需要做相关处理才能确保程序的正确性。

多数情况下,只要正确地打开了优化选项,你都不用做额外的优化,因为编译器就可以让程序的运行速度提高数倍。默认情况下,许多编译器都不会进行任何优化,因为如果不进行优化,编译器就可以稍微缩短一点编译时间。

7.2 使用更好的算法

选择一个最优算法对性能优化的效果最大。各种优化手段都能改善程序的性能。它们可以压缩以前看似低效的代码的执行时间,但是除非你能找到一种更加高效的算法,否则要想实现性能的指数级增长通常是不太可能的。

几个改善程序性能的重要技巧,其中包括预计算(precomputation,将计算从运行时移动至链接、编译或是设计时)、 延迟计算(lazy computation,如果通常计算结果不会被使用,那么将计算推迟至真正需要使用计算结果时)和缓存(caching,节省和复用昂贵的计算)。

7.3 使用更好的库

C++ 编译器提供的标准 C++ 模板库和运行时库是可维护的、全面的和非常健壮的。对进行性能优化的开发人员来说,掌握标准 C++ 模板库是必需的技能。

有一些开源库实现了非常重要的功能。它们提供的复杂的实现可能比供应商提供的 C++ 运行时库更快、更强。开发人员还可以开发适合自己项目的库,通过放松标准库中的某些安全性和健壮性约束来换取更快的运行速度。要想隐藏高度优化后的程序的复杂性,函数和类库是非常合适的地方。

7.4 减少内存分配和复制

减少对内存管理器的调用是一种非常有效的优化手段,以至于开发人员只要掌握了这一个技巧就可以变为成功的性能优化人员。绝大多数 C++ 语言特性的性能开销最多只是几个指令,但是每次调用内存管理器的开销却是数千个指令。

对缓存复制函数的一次调用也可能消耗数千个 CPU 周期。因此,很明显减少复制是一种提高代码运行速度的优化方式。比如c++11支持的移动语义。

7.5 优化循环处理

单条 C++ 语句的性能开销通常都很小。但是如果在循环中执行 上万次这条语句,或是每次程序处理事件时都执行这条语句,那么这就是个大问题了。绝大多数程序都会有一个或多个主要的事件处理循环和一个或多个处理字符的函数。找出并优化这些循环几乎总是可以让性能优化硕果累累。

7.6 使用更好的数据结构

选择最合适的数据结构对性能有着深刻的影响,因为插入、迭代、排序和检索元素的算法的运行时开销取决于数据结构。除此之外,不同的数据结构在使用内存管理器的方式上也有所不同。

程序=数据结构+算法

好的数据结构,可以使用更合适的算法,从而减少内存占用,提高程序性能。

7.7 提高并发性

任何时候,如果一个程序的处理进度因需要等待某些事件被暂停,而没有利用这些时间进行其他处理,都是一种浪费。

现代计算机都可以使用多个处理核心来执行指令。如果一项工作被分给几个处理器执行,那么它可以更快地执行完毕。伴随并发执行而来的是用于同步并发线程让它们可以共享数据的工具。但是需要注意数据的线程安全性,比如C++的STL容器都不是线程安全的,如果需要做多线程处理,需要重写容器或或其他特殊设计。

7.8 优化内存管理

内存管理器作为 C++ 运行时库中的一部分,管理着动态内存分配。合理使用内存管理器,避免频繁开辟和释放空间,减少内存碎片,提高程序运行效率。

处理速度排序:cpu从寄存器读取最快, 接下来是缓存 , 接下来是内存。

假设一个8核的cpu, 每个核都有自己独立的L1 Cache和L2 Cache, 而L3 Cache是8核共享的。离核心越近, 等级越高, 速度越快, L1 Cache缓存最小, 速度最快。内存的数据会先加载到共享的L3 Cache中, 再加载到每个核心独有的L2 Cache, 最后进入到最快的L1 Cache.

内存优化整体策略

  • 一起使用的函数存储在一起。函数的存储通常按照源码中的顺序来的,如果函数A,B,C是一起调用的,那尽量让ABC的声明也是这个顺序
  • 一起使用的变量存储在一起。使用结构体、对象来定义变量,并通过局部变量方式来声明,都是一些较好的选择。
  • 合理使用字节对齐, 让一个CacheLine能获取到更多有效的数据
  • 动态内存分配、STL容器、string等,核心代码处尽量不用

底层规范-寄存器

寄存器是cpu的组成部分, 是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果

1.尽量使用栈内存,但避免栈溢出

2.尽量使用无符号数

3.尽量避免使用浮点数,考虑用整形转换

底层规范-缓存

缓存是cpu的一部分, 位于cpu中。在没有缓存之前, cpu一直都是在内存中读取数据的, 但由于两者速度差异, cpu每次都要等内存的’回信’, 缓存的设计是用来解决cpu与内存速度差异问题.

1.顺序存取数据

2.避免访问数据时缓存切换

3.避免字节不对齐对缓存影响

底层规范-内存

内存又称主存,也称内存储器和主存储器。它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。内存的运行决定计算机整体运行快慢。

1.尽量使用栈内存(L1 Cache,寄存器)

2.尽量避免全局变量/静态变量

3.避免动态内存申请/释放

4.避免使用STL容器类,或自定义内存分配器

5.避免使用string

6.避免没必要的复制、赋值( memset,memcpy)

底层规范-内存-避免动态内存申请/释放

1.分配释放需要寻找合适大小内存块,会花费更多时间

2.分配释放大小不同内存块,易造成堆空间碎片化,降低缓存效率

3.堆空间碎片化,可能在不确定时间进行gc,使得性能不稳定

4.动态分配内存容易造成数据未对产,可能影响Cache

5.编译器较难优化使用指针的代码

6. 使用者需要确保申请释放成对,避免内存泄漏导致堆内存耗尽

7.使用者需要确保内存释放后不能访问

底层规范-内存-vector

1.动态内存申请释放(vector动态扩容)

2.调整大小时,复制所有存储内容

3.考虑使用reserve避免频繁申请内存

底层规范-内存-string

1.动态内存申请释放

2.调整大小时,复制所有存储内容

3.考虑避免频繁动态申请

4.考虑使用C风格字符串替换

底层规范-内存- C+ +规范-避免没必要的复制与赋值

1.类定义中禁止不期望的复制

2.使用
pass-by-reference-to-const 代替pass-by-value

3.在初始化列表中替代在构造函数体内初始化

4.使用复合运算符代替独身运算符

5.返回值优化( RVO)

6.考虑使用modern C++的移动语义

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
性能优化,我们应该知道的更多一点
充分榨干 CPU 的每一个 TICK:软件性能优化方法知多少
软件工程优化实用技巧-上
计算机世界网-Java性能的优化(上)
优化--C程序员之终极标靶
华为,苹果,三星处理器的不同?
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服