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 的问题。