常见性能优化思路
文章目录
性能
性能优化有三个层次:
-
系统层次
-
算法层次
-
代码层次
系统层次关注系统的控制流程和数据流程,优化主要考虑如何减少消息传递的个数;如何使系统的负载更加均衡;如何充分利用硬件的性能和设施;如何减少系统额外开销(比如上下文切换等)。
算法层次关注算法的选择(用更高效的算法替换现有算法,而不改变其接口);现有算法的优化(时间和空间的优化);并发和锁的优化(增加任务的并行性,减小锁的开销);数据结构的设计(比如lock-free的数据结构和算法)。
代码层次关注代码优化,主要是cache相关的优化(I-cache,D-cache相关的优化);代码执行顺序的调整;编译优化选项;语言相关的优化技巧等等。
性能的主要影响因素及优化策略
主要影响因素
I/O,系统调用,并发/锁,内存分配,内存拷贝,函数调用消耗,编译优化,算法
I/O性能优化
I/O性能主要耗费点:系统调用,磁盘读写,网络通讯等
优化点:减少系统调用次数,减少磁盘读写次数,减少阻塞等待
优化手段:
-
使用非阻塞模式
-
使用带缓存的I/O,减少磁盘读写次数
-
I/O多路复用,select/poll/epoll
-
异步I/O
系统调用
耗费点:用户态和系统态切换时耗
优化点:减少不必要的系统调用
优化手段:
-
I/O操作,根据具体情况,使用stdio库代替read/write
-
缩减不必要的系统调用.
并发/锁
并发处理(多线程、多进程)在一定条件下可提升性能,但如果存在共享资源,则需要有互斥锁的开销。
锁的优化:
-
线程本地变量,避免存在共享资源
-
减少锁的粒度(锁级别)
-
无锁算法,如使用原子操作。
-
算法上减少对共享资源的访问, 如多版本算法
内存分配
涉及系统调用和系统内存分配的锁操作。
优化点:减少内存分配/释放的次数和频繁度
优化手段:
-
一次分配多次使用,如内存池
-
系统内存分配替代库,如tcmalloc提高多线程环境内存分配
-
提升对象重用程度,避免重复构造和析构
内存拷贝
优化点:减少内存的拷贝操作
优化手段:
-
利用指针、引用代替数值拷贝
-
写时复制技术,两个对象同时引用一份数据,只有当其中一个对象需要改写数据时,才拷贝出一个数据副本。(std::string采用写时复制, 因此一般情况下函数按值传递和返回std::string,不存在字符串复制操作)
函数调用消耗
函数调用时存在栈分配初始化以及后续的栈回收操作。
优化手段:简单的函数,使用宏或内联方式
编译优化
使用编译器的优化选项,带来额外的性能提升
算法
针对特定的需求提升算法优化程度,如减少循环处理次数,使用高性能排序和搜索算法等。
代码优化的具体策略
字符数组的初始化
一些情况是:写代码时,很多人为了省事或者说安全起见,每次申请一段内存之后都先全部初始化为0。
另一些情况是:用了一些API,不了解底层实现,把申请的内存全部初始化为0了,比如char buf[1024]=””的方式.
上面提到两种内存初始化为0的情况,其实有些时候并不是必须的。比如把char型数组作为string使用的时候只需要初始化第一个元素为0即可,或者把char型数组作为一个buffer使用的大部分时候根本不需要初始化。
频繁的内存申请、释放操作
曾经遇到过一个性能问题是:一个服务在启动了4-5小时之后,性能突然下降。
查看系统状态发现,这时候CPU的sys态比较高,同时又发现系统的minflt值迅速增加,于是怀疑是内存的申请、释放造成的性能下降。
最后定位到是服务的处理线程中,在处理请求时有大量申请和释放内存的操作。定位到原因之后就好办了,直接把临时申请的内存改为线程变量,性能一下子回升了。
能够迅速的怀疑到是临时的内存申请造成的性能下降,还亏之前看过这篇帖子。
至于为什么是4-5小时之后,性能突然下降,则怀疑是内存碎片的问题。
提前计算
这里需要提到的有两类问题:
-
局部的冗余计算:循环体内的计算提到循环体之前
-
全局的冗余计算
问题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,我的看法是,即使编译器会内联小函数,如果把函数定义写在cpp文件中并在另外一个cpp中调用该函数,这时编译器无法内联该调用。
针对2,我的实际经验是,内联了一个每个请求调用几百次的get操作之后,响应时间减少5%左右。
位运算代替乘除法
据说如果是常量的运算的话,编译器会自动优化选择最优的计算方式。这里的常量计算不仅仅是指”48”这样的操作,也可能是”ab”但编译的时候编译器已经可以知道a和b的值。
不过在编译阶段无法知道变量值的时候,将*、/、% 2的幂的运算改为位运算,对性能有时还是蛮有帮助的。
我遇到的一次优化经历是,将每个请求都会调用几十到数百次不等的函数中一个*8改为«3和一个%8改为&7之后,服务器的响应时间减少了5%左右。
下面是我实测的一些数据:
%2的次方可以用位运算代替,a%8=a&7(两倍多效率提升)
/2的次方可以用移位运算代替,a/8=a»3(两倍多效率提升)
2的次方可以用移位运算代替,a8=a«3(小数值测试效率不明显,大数值1.5倍效率)
整数次方不要用pow,ii比pow(i,2)快8倍,ii*i比pow快40倍
strncpy, snprintf效率对比:目标串»源串 strncpy效率低,源串»目标串 snprintf效率低
编译优化
gcc编译的时候,很多服务都是采用O2的优化选项了。不过在使用公共库的时候,可能没注意到就使用了一个没开任何优化的产出了。我就遇到过至少3个服务因为打开了tcmalloc库的O2选项之后性能提升有10%以上的。
转载:
http://blog.csdn.net/luckysym/article/details/38309137
http://www.cnblogs.com/wujianlundao/archive/2012/11/18/2776372.html
文章作者 Forz
上次更新 2017-06-25