前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【干货】C++性能优化 | 吴咏炜在2020全球C++及系统软件技术大会中的分享

【干货】C++性能优化 | 吴咏炜在2020全球C++及系统软件技术大会中的分享

原创
作者头像
Boolan博览
修改2021-09-29 17:18:59
1.2K0
修改2021-09-29 17:18:59
举报
文章被收录于专栏:C++及系统软件C++及系统软件

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首席软件咨询师吴咏炜老师为大家揭秘。

3f103978bc7621a4fe88516e3c8623c6[1].png
3f103978bc7621a4fe88516e3c8623c6[1].png

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

引言

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

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

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

性能测试方式

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

性能优化

1、循环优化

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2、多线程优化

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

①能使用 atomic 就不用 mutex;

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

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

3、算术表达式优化

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

不需要做的优化

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

查看下方骚操作

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

2、提取公共表达式

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

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

2021全球C++及系统软件技术大会
2021全球C++及系统软件技术大会

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

800.417-01.jpg
800.417-01.jpg

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 性能测试方式
  • 性能优化
  • 不需要做的优化
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档