Skip to content

C++ 程序常见的性能调优方式

转载自:http://www.708luo.com/?p=36

冗余的变量拷贝

相对 C 而言,写 C++ 代码经常一不小心就会引入一些临时变量,比如函数实参、函数返回值。在临时变量之外,也会有其他一些情况会带来一些冗余的变量拷贝。

之前针对冗余的变量拷贝问题写过一些帖子,详情请点击这里

多重过滤

很多服务都会过滤的部分结果的需求,比如游戏交谈中过滤需要过滤掉敏感词。假设现在有两个过滤词典,一个词典 A 内容较少,另一个词典 B 内容较多,现在有 1000 个词需要验证合法性。

词落在词典 A 中的概率是 1%,落在词典 B 中的概率是 10%,而判断词是否落在词典 A 或 B 中的操作耗时差不多,记作 N。

那么要判断词是否合法,有两种方式:

1. 先判断词是否在 A 中,如果在返回非法;如果不在再判断是否在 B 中,如果在返回非法,否则返回合法。

2. 和方式一类似,不过是先判断是否在 B 中。

现在我们来计算两种方式的耗时:

1. 1000*N+1000*(1-1%)*N

2. 1000*N+1000*(1-10%)*N

很明显,方式二的过滤操作排序优化方式一。

说得有些啰嗦,其实简单点说就是一句话:多重过滤中把强过滤前移;过滤强度差不多时,过滤消耗较小的前移

如果有些过滤条件较强,但是过滤消耗也较大怎么办?该前移还是后移?个人到没遇到过这种情况,如果确实需要考虑,也可以用之前计算方式一、二整体耗时的方法也计算一遍。

字符数组的初始化

一些情况是:写代码时,很多人为了省事或者说安全起见,每次申请一段内存之后都先全部初始化为 0。

另一些情况是:用了一些 API,不了解底层实现,把申请的内存全部初始化为 0 了,比如 char buf[1024]=""的方式,有篇帖子写得比较细,请看这里

上面提到两种内存初始化为 0 的情况,其实有些时候并不是必须的。比如把 char 型数组作为 string 使用的时候只需要初始化第一个元素为 0 即可,或者把 char 型数组作为一个 buffer 使用的大部分时候根本不需要初始化。

频繁的内存申请、释放操作

曾经遇到过一个性能问题是:一个服务在启动了 4-5 小时之后,性能突然下降。

查看系统状态发现,这时候 CPU 的 sys 态比较高,同时又发现系统的 minflt 值迅速增加,于是怀疑是内存的申请、释放造成的性能下降。

最后定位到是服务的处理线程中,在处理请求时有大量申请和释放内存的操作。定位到原因之后就好办了,直接把临时申请的内存改为线程变量,性能一下子回升了。

能够迅速的怀疑到是临时的内存申请造成的性能下降,还亏之前看过这篇帖子

至于为什么是 4-5 小时之后,性能突然下降,则怀疑是内存碎片的问题。

提前计算

这里需要提到的有两类问题:

1. 局部的冗余计算:循环体内的计算提到循环体之前

2. 全局的冗余计算

问题 1 很简单,大部分人应该都接触到过。有人会问编译器不是对此有对应的优化措施么?对,公共子表达式优化是可以解决一些这个问题。不过实测发现如果循环体内是调用的某个函数,即使这个函数是没有 side effect 的,编译器也无法针对这种情况进行优化。(我是用 gcc 3.4.5 测试的,不排除更高版本的 gcc 或者其他编译器可以针对这种情况进行优化)

对于问题 2,我遇到的情况是:服务代码中定义了一个 const 变量,假设叫做 MAX_X,处理请求是,会计算一个 pow(MAX_X) 用作过滤阈值,而性能分析发现,这个 pow 操作占了整体系统 CPU 占用的 10% 左右。对于这个问题,我的优化方式很简单,直接计算定义一个 MAX_X_POW 变量用作过滤即可。代码修改 2 行,性能提升 10%。

空间换时间

这其实是老生常谈、在大学里就经常提到的问题了。

不过第一次深有体会的应用却是在前段时间刚遇到。简单来说是这样一个应用场景:系统内有一份词表和一份非法词表,原来的处理逻辑是根据请求中的数据查找到对应的词(很多),然后用非法词表过滤掉其中非法的部分。对系统做性能分析发现,依次判断查找出来的词是否在非法词表中的操作比较耗性能,能占整体系统消耗 CPU 的 15-20%。后来的优化手段其实也不复杂,就是服务启动加载词表和非法词表的时候,再生成一张合法词表,请求再来的时候,直接在合法词表中查到结果即可。不直接用合法词表代替原来那份总的词表的原因是,总的词表还是其他用途。

内联频繁调用的短小函数

很多人知道这个问题,但是有时候会不太关注,个人揣测可能的原因有:

1. 编译器会内联小函数

2. 觉得函数调用的消耗也不是特别大

针对 1,我的看法是,即使编译器会内联小函数,如果把函数定义写在 cpp 文件中并在另外一个 cpp 中调用该函数,这时编译器无法内联该调用。

针对 2,我的实际经验是,内联了一个每个请求调用几百次的 get 操作之后,响应时间减少 5% 左右。

位运算代替乘除法

据说如果是常量的运算的话,编译器会自动优化选择最优的计算方式。这里的常量计算不仅仅是指"4*8"这样的操作,也可能是"a*b"但编译的时候编译器已经可以知道 a 和 b 的值。

不过在编译阶段无法知道变量值的时候,将*、/、% 2 的幂的运算改为位运算,对性能有时还是蛮有帮助的。

我遇到的一次优化经历是,将每个请求都会调用几十到数百次不等的函数中一个*8 改为<<3 和一个%8改为&7 之后,服务器的响应时间减少了 5% 左右。

下面是我实测的一些数据:

%2的次方可以用位运算代替,a%8=a&7(两倍多效率提升)

/2 的次方可以用移位运算代替,a/8=a>>3(两倍多效率提升)

*2 的次方可以用移位运算代替,a*8=a<<3(小数值测试效率不明显,大数值 1.5 倍效率)

整数次方不要用 pow,i*i 比 pow(i,2) 快 8 倍,i*i*i 比 pow 快 40 倍

strncpy, snprintf 效率对比:目标串>>源串 strncpy 效率低,源串>>目标串 snprintf 效率低

编译优化

gcc 编译的时候,很多服务都是采用 O2 的优化选项了。不过在使用公共库的时候,可能没注意到就使用了一个没开任何优化的产出了。我就遇到过至少 3 个服务因为打开了 tcmalloc 库的 O2 选项之后性能提升有 10% 以上的。

不过开 O2 优化,有些时候可能会遇到一些非预期的结果,比如这篇帖子提到的 memory aliasing 的问题。