【干货储备】C++性能优化

本文涉及的产品
性能测试 PTS,5000VUM额度
简介: 做C++,当然不能不关心性能。但是,什么时候开始关心性能优化?2020全球C++及系统软件技术大会中《C++性能调优纵横谈》的演讲,现场座无虚席,好评连连。下面让演讲者,Boolan首席软件咨询师吴咏炜老师为大家揭秘。
We should forget about small efficiencies, say about 97percent of the
time:premature optimization is the root of all evil.
我们应该在97%的时间忘记优化:过早优化是万恶之源。

做C++,当然不能不关心性能。但是,什么时候开始关心性能优化?2020全球C++及系统软件技术大会中《C++性能调优纵横谈》的演讲,现场座无虚席,好评连连。下面让演讲者,Boolan首席软件咨询师吴咏炜老师为大家揭秘。

国内知名 C++专家。曾任英特尔亚太研发中心资深系统架构师,近 30 年 C/C++系统级软件开发和架构经验。专注于 C/C++ 语言(包括 C++98/C++11/14/17/20)、软件架构、性能优化、设计模式和代码重用。长期担任资深技术教练,具有丰富技术咨询经验。

https://ucc.alicdn.com/pic/developer-ecology/b187b36637414229b45a08db0ff1c6de.png

引言

先说为什么要用C++?摩尔定律推动计算机的性能不停提高,脚本语言大行其道。但是计算机的性能毕竟有限,到21世纪初,就不得不通过语言层面以及个人写代码的技巧等各方面来提升性能。

https://ucc.alicdn.com/pic/developer-ecology/61fb8305d02a4449b39cad5008881ec7.png

但是我们也无法做到100%优化,因为C++开发效率较低,如果想在整个代码做优化,得不偿失。原因我们看下面这个公式。里面P代表优化的部分所占比例,Sp是对这部分P的性能提升大小。

https://ucc.alicdn.com/pic/developer-ecology/6fe30a16d27c41afaf89b0433d21a817.png

举两个最简单的数据说明:

①如果优化的部分有一个非常重要的函数,这个函数占到系统开销的50%,这时,我们把这个部分的性能提升了50%,这种情况下,结果是提升了20%,这已经是一个非常好的成果。

②反之,如果有一个函数性能提升100%,如果在执行过程中只占了系统开销的1%(不管它占代码总量多少),那即使这部分性能提升了100%,最后结果也只提升了0.5%。

所以,很重要的一件基本的事情,就是要做性能测试。
测不准的问题

性能测试是一件很难的事情,也是一件非常有技巧的事情。以下面的简单代码为例,我们看一下memset和手工清零,性能有没有差异,差异是多少?

https://ucc.alicdn.com/pic/developer-ecology/8099f8f5c6ac4005835c28744b8d8c19.png

下方展示了一个令人惊讶的测试结果

https://ucc.alicdn.com/pic/developer-ecology/f2770b6e85734d1eb3e2ade746b6d75a.pngg

根据示例代码的测试结果我们可以看出,当优化开到 -O2时,memset居然比手工循环慢了10万倍。memset在GCC8之下,开到 -O2不会被优化,仍会做memset,但编译器会完全干掉对buffer的写入。这就是常见的陷阱。

https://ucc.alicdn.com/pic/developer-ecology/8890b8691f74492fb7ec2e8e1f00fb81.png

那怎么绕过测试测不准的问题?volatile可以使测试结果相对合理。

https://ucc.alicdn.com/pic/developer-ecology/dc74c12a8ffa43b798761959f2cd5dc4.png

然而volatile本身会妨碍优化。我们看下方汇编代码,80个单字节的0,去掉volatile,在GCC10下直接做了5次的16字节0写入,而且没有循环。这就是C++编译器的优化魔法。

https://ucc.alicdn.com/pic/developer-ecology/2427afc35fff496e80695726c31a0f5c.png

在前面的示例代码里,两种方式在优化编译下的性能,实际上是完全一致的。

下面列举了一些编译器的优化魔法,在没有同步原语的情况下,编译器可以(通常为了性能)在(当前线程)结果不变的情况下自由地调整执行顺序。比如局部变量可能被全部消除;而全局变量不会被优化没,但是写入的顺序可能会调整,编译器觉得怎么方便怎么写入,只要对外表现行为与程序的设计行为完全一致。

例如:x = a; y = 2; 可以变为 y = 2; x = a

x=a,是从a里面读东西,写到x,做了内存读操作,再做内存写操作。我们看汇编代码,会发现会先做从a读到eax,同时对y写入读,然后对x写入,从而达到最高的并发性。

https://ucc.alicdn.com/pic/developer-ecology/ecd179d2ea5940bb9dcb4712432fd351.png

另外,volatile声明会禁止编译器进行相关优化。

对volatile变量的读,编译器肯定会生成读语句;对volatile变量的写,编译器肯定会生成写语句。这是一种很特殊的场景,所以一般用于驱动程序,内存映射文件等,正常情况下volatile需要谨慎使用。特别需要指出的一点,volatile在C++和Java里面的语义完全不一样,在C++里面没有多线程同步的语义。

以上就是测试可能存在的坑,从防优化的角度我们总结出以下技巧:

https://ucc.alicdn.com/pic/developer-ecology/0a18ce574a3c4c9db62cc1aaa3eb4015.png

性能测试方式

不管是锁,还是额外函数调用,都会有额外开销,尤其锁的性能开销是有点大的,所以我们需要比clock更好的进行性能测试的方式。通过分析测时长相关的函数,我们可以发现rdtsc是x86 和x64系统上的的首选计时方式。

https://ucc.alicdn.com/pic/developer-ecology/87337f23442141f4a561ce152deff13b.png

需要注意,tsc的主屏频率和CPU参考主频不一定一致,需要自己测试,或者从Linux里面使用dmesg查找tsc的频率信息。

以rdtsc为计时方式,我们可实现一个性能分析器profiler,测量出函数调用和虚函数调用的额外开销(不同的软硬件会影响测试数据),可以发现开销是很低的。

https://ucc.alicdn.com/pic/developer-ecology/28154f52d9ed4fe9be9e6600a019e078.png

我们前面说的测试方式属于插桩测试。插桩测试的开销随测试范围而变,虽然函数调用开销较低,但依然存在开销,而且测量出的时钟周期都可能带来问题,所以插桩本身可能影响测试结果,但是结果相对较为精确、稳定,适合对单个函数进行性能调优。

另外一种测试方式是采样测试。采样测试需要依赖于一个外部的东西,在程序的执行过程中,它会定期中断程序,然后检查调用栈,知道程序当前执行到哪里,最后看百分比的分布,从而知道函数的大概比例。采样测试比较优势的地方,是总体开销可控,而且适合用来寻找程序的热点。

总结:整体找程序的热点与问题在哪里,用采样测试;已经找到热点,需要进行精细优化,用插桩测试。

关于采样测试常用的一些工具。一个是GCC自带的工具gprof,它是采样结合了部分插桩,可以很快上手尝试,但是因为总体效果不太好,所以并不推荐。比较推荐的Google的gperftools.

https://ucc.alicdn.com/pic/developer-ecology/236ea43004b54506920cb2b5ade1f8b5.png

编译的时候不需要做特殊处理,用普通的 -g和 -o参数就可以。执行的时候,可以在命令行上指定预加载profiler库,再指定CPUPROFILE输出到哪个文件,然后执行代码,这样就可以生成test.prof文件,最后再用google-pprof工具把test.prof生成输出文件,可以是svg、jpg、png之类的格式。

其中,SVG的效果会比较好一些,有层次关系、树形结构,字体的大小代表了耗时百分比的高低,可以很清晰的看到整体执行的性能,进行分析。

性能优化

1、循环优化

循环会放大代码中的低效率,所以不必要的反复执行的代码要提到循环外面,否则会有额外的开销。以下面这个糟糕代码为例:

https://ucc.alicdn.com/pic/developer-ecology/9bbe1c4869bc4e8baed8c50109fb51ae.png

首先,strlen这个函数会被反复调用,其次,strlen是个很糟糕的函数,它的执行时间与你的字符串长度成正比。所以如果给了一个长的字符串,即使不考虑strlen本身的函数调用开销的问题,也需要考虑是不是应该把这个长度随时随地带在API里,而不是调strlen来获得它的长度。那这种问题如何优化?

长度不变的情况,在for循环的开头初始化一下,然后后面就是循环写入。这个优化也是GCC可能自动做的,当GCC能够判定你肯定没有在修改这个字符串的时候,它甚至可以帮你直接做到这一点。但是当你把s,一个char*,传到另外一个函数去,GCC判定不了那个函数背后做了什么,就无法优化。所以还是需要手工将优化写出来,这是一种非常基本的优化方式。

https://ucc.alicdn.com/pic/developer-ecology/4debf5c5c6754d95b41d255ec2110b8f.png

如果长度可变的情况,原理是一样的。可以先把长度保存下来,然后在长度进行变化的时候,直接调整长度值。但是,如果字符串太长的话,我们仍需要做一些其他的优化操作,但是概念是一样的,就是尽量避免重复做不必要的操作,这是最基本的优化思路。

2、多线程优化

在某些内存管理器里,每次调用时会有个加解锁的问题,而加解锁是绝对的性能杀手。所以,

①能使用 atomic 就不用 mutex;

②如果读比写多很多,考虑使用读写锁(shared_mutex)而不是独占锁(mutex);

③使用线程本地(thread_local)变量

3、算术表达式优化

下面这个等式,从代码角度看是否成立?

https://ucc.alicdn.com/pic/developer-ecology/f16c4a808de94af28ea7ca28f196e296.png

这个地方的关键是是否使用了浮点数类型。浮点数的精度有限,这就意味着一个操作先做还是后做,可能会影响结果,编译器就会保守处理,不敢轻易做优化。所以除非你开了 -Ofast,告诉编译器可以不管 IEEE 浮点数据运算规则,在碰到浮点数的时候,要做一下手工处理。

不需要做的优化

1、移位和乘法,不需要开优化, -O0就会做。

https://ucc.alicdn.com/pic/developer-ecology/596e9b70f18243b1bddb03adb4174e5d.png

查看下方骚操作

https://ucc.alicdn.com/pic/developer-ecology/05c91544e4a941e1b1c727dc21677e81.png

2、提取公共表达式

https://ucc.alicdn.com/pic/developer-ecology/6cb54d47800d42e48e7298a84ae9ca61.png

3、略去本地变量的初始化

无用的初始化,编译器会自动消掉。

https://ucc.alicdn.com/pic/developer-ecology/0ccf8d6dbf584691a57992037d4bb842.png

以上就是吴咏炜老师在2020全球C++及系统软件技术大会中分享的内容,这里只讨论了部分性能优化点,性能调优的手段还有很多,欢迎大家有问题交流讨论。2021全球C++及系统软件技术大会将于11月25-26日上海举办,吴咏炜老师会再次出席为大家带来现代C++新特性技能分享,感兴趣的小伙伴不要错过哦!

800.417-01.jpg

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
相关文章
|
6月前
|
消息中间件 缓存 NoSQL
如何做性能优化?
如何做性能优化?
|
3月前
|
缓存 Android开发 UED
安卓应用开发中的性能优化实践
【8月更文挑战第31天】在安卓的世界里,性能是王道。本文将带你深入理解如何通过代码优化和工具使用来提升你的安卓应用性能。我们将一起探索内存管理、布局优化、多线程处理等关键领域,并配以实用的代码示例,让你的应用飞一般地运行起来!
|
6月前
|
缓存 编译器 数据处理
【C/C++ 性能优化】循环展开在C++中的艺术:提升性能的策略与实践
【C/C++ 性能优化】循环展开在C++中的艺术:提升性能的策略与实践
593 0
|
4月前
|
缓存 数据库 Android开发
安卓应用开发中的性能优化策略
【7月更文挑战第21天】在移动设备上,性能问题直接影响用户体验。本文将探讨在安卓应用开发过程中,开发者可以采用的多种性能优化方法。我们将从代码层面、资源管理、网络通信、UI渲染等方面入手,深入分析如何有效减少应用的内存占用和提升响应速度。此外,文章还将介绍一些实用的工具和平台,帮助开发者检测和解决性能瓶颈。
75 1
|
5月前
|
存储 JSON 数据格式
如何提升写入效率?Schemaless 写入性能优化实践分享
TDengine 是一款时序数据库,其Schemaless模式适应物联网数据动态变化。通过分析火焰图,发现parser和insert操作是性能瓶颈。优化措施包括减少标签解析、排序和子表生成的重复执行,提前判断schema变更,改进数据插入方法,减少内存分配和拷贝。通过这些优化,如在3.0版本中,line协议性能提升了2.5倍,telnet提升2倍,json提升近5倍。使用工具如火焰图和perf进行性能分析,以识别和解决瓶颈,实现性能提升。
33 0
|
6月前
|
缓存 监控 NoSQL
一次性能优化实践
【5月更文挑战第21天】为解决在线教育平台在高并发下数据库查询响应时间增加的问题,开发者采用Redis缓存策略。通过数据分层、LRU淘汰策略、异步更新及监控调优,成功提升性能,缓存命中率超90%,页面加载时间从3秒降至1秒,改善了用户体验。此实践强调了合理缓存策略、监控调优以及考虑数据访问模式在系统设计中的重要性。
74 2
|
存储 缓存 JavaScript
我工作中用到的性能优化全面指南(1)
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。 最小化和压缩代码 在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。
64 0
|
Web App开发 存储 缓存
我工作中用到的性能优化全面指南(2)
使用WebGL进行3D渲染 WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。
104 0
|
消息中间件 缓存 容灾
金融系统性能优化之道
系统设计得再好,如不能及时完成业务处理也不行。为什么不同业务有不同优化需求,以及常见的优化方式和问题有哪些。
106 0
|
存储 消息中间件 缓存
性能优化的十种手段
性能优化的十种手段