性能

性能优化有三个层次:

  1. 系统层次

  2. 算法层次

  3. 代码层次

系统层次关注系统的控制流程和数据流程,优化主要考虑如何减少消息传递的个数;如何使系统的负载更加均衡;如何充分利用硬件的性能和设施;如何减少系统额外开销(比如上下文切换等)。

算法层次关注算法的选择(用更高效的算法替换现有算法,而不改变其接口);现有算法的优化(时间和空间的优化);并发和锁的优化(增加任务的并行性,减小锁的开销);数据结构的设计(比如lock-free的数据结构和算法)。

代码层次关注代码优化,主要是cache相关的优化(I-cache,D-cache相关的优化);代码执行顺序的调整;编译优化选项;语言相关的优化技巧等等。

性能的主要影响因素及优化策略

主要影响因素

I/O,系统调用,并发/锁,内存分配,内存拷贝,函数调用消耗,编译优化,算法

I/O性能优化

I/O性能主要耗费点:系统调用,磁盘读写,网络通讯等

优化点:减少系统调用次数,减少磁盘读写次数,减少阻塞等待

优化手段:

  1. 使用非阻塞模式

  2. 使用带缓存的I/O,减少磁盘读写次数

  3. I/O多路复用,select/poll/epoll

  4. 异步I/O

系统调用

耗费点:用户态和系统态切换时耗

优化点:减少不必要的系统调用

优化手段:

  1. I/O操作,根据具体情况,使用stdio库代替read/write

  2. 缩减不必要的系统调用.

并发/锁

并发处理(多线程、多进程)在一定条件下可提升性能,但如果存在共享资源,则需要有互斥锁的开销。

锁的优化:

  1. 线程本地变量,避免存在共享资源

  2. 减少锁的粒度(锁级别)

  3. 无锁算法,如使用原子操作。

  4. 算法上减少对共享资源的访问, 如多版本算法

内存分配

涉及系统调用和系统内存分配的锁操作。

优化点:减少内存分配/释放的次数和频繁度

优化手段:

  1. 一次分配多次使用,如内存池

  2. 系统内存分配替代库,如tcmalloc提高多线程环境内存分配

  3. 提升对象重用程度,避免重复构造和析构

内存拷贝

优化点:减少内存的拷贝操作

优化手段:

  1. 利用指针、引用代替数值拷贝

  2. 写时复制技术,两个对象同时引用一份数据,只有当其中一个对象需要改写数据时,才拷贝出一个数据副本。(std::string采用写时复制, 因此一般情况下函数按值传递和返回std::string,不存在字符串复制操作)

函数调用消耗

函数调用时存在栈分配初始化以及后续的栈回收操作。

优化手段:简单的函数,使用宏或内联方式

编译优化

使用编译器的优化选项,带来额外的性能提升

算法

针对特定的需求提升算法优化程度,如减少循环处理次数,使用高性能排序和搜索算法等。

代码优化的具体策略

字符数组的初始化

一些情况是:写代码时,很多人为了省事或者说安全起见,每次申请一段内存之后都先全部初始化为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%左右。

位运算代替乘除法

据说如果是常量的运算的话,编译器会自动优化选择最优的计算方式。这里的常量计算不仅仅是指”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