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

2021-09-29 17:18:59 浏览数 (1)

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

Boolan首席软件咨询师吴咏炜Boolan首席软件咨询师吴咏炜

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

2020全球C  及系统软件技术大会现场2020全球C 及系统软件技术大会现场

引言

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

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

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

性能测试方式

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

性能优化

1、循环优化

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2、多线程优化

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

①能使用 atomic 就不用 mutex;

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

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

3、算术表达式优化

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

不需要做的优化

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

查看下方骚操作

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

2、提取公共表达式

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

2020全球C  及系统软件技术大会-吴咏炜演讲内容2020全球C 及系统软件技术大会-吴咏炜演讲内容

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

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

0 人点赞