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