一、性能指标
一般来说性能有两个指标:响应时间和吞吐量
1. 响应时间指的是“系统响应时间”,定义为应用系统从发出请求开始到客户端接收到响应所消耗的时间。
2.吞吐量是指“单位时间内系统处理的客户请求的数量”,直接体现软件系统的性能承载能力。
二、性能瓶颈
进行任何性能优化前,有个很重要的前提事先找到性能瓶颈点,然后才能针对性能进行优化;我们常用的一些工具如下:
1.ping:做网络延时,丢包率;
2.sockperf pp时延测试(udp,tcp不同大小),记录每一次平均时延,并且取平均值;
3.netperf测试吞吐,netserver,netperf 测试吞吐率以及响应速率
4.iperf测试丢包率,iperf3
5.perf
perf stat 采集程序运行事件,用于分析指定程序的性能概况
perf top 对程序性能进行实时分析
也可观察到程序中函数cache-misses占比:perf top -e cache-misses -p $pid
6.gperftools,主要优点是非常友好的图形输出,同时低开销和使用非常简单
可以显示各个调用连上函数执行占比,找到耗时异常的函数,找到性能瓶颈点;
三、性能优化的一些方法
1.局部性原理
局部性有两种,即时间局部性和空间局部性:
时间局部性是指当一个数据被访问后,它很有可能会在不久的将来被再次访问,比如循环代码中的数据或指令本身;
空间局部性是指当程序访问地址为xxxx的数据时,很有可能会紧接着访问xxxx周围的数据,比如遍历数组或指令的顺序执行;
由于这两种局部性存在于大多数的程序中,硬件系统可以很好地预测哪些数据可以放入缓存,从而运行得很好。
2.缓存优化-缓存亲和性
缓存访问是设计多处理器调度时遇到的关键问题,是所谓的缓存亲和度(cache affinity),即一个进程在某个CPU上运行时 会在该CPU缓存中维护许多状态信息;下次进程在相同CPU上运行时,由于缓存中的数据而执行得更快。
相反,当进程在不同的CPU上运行时,由于需要重新加载数据而变得更慢(好在硬件保证的缓存一致性可以保证正确执行)。因此 性能优化时应该考虑到这种缓存亲和性,尽可能将进程保持在同一个CPU上。
3.NUMA优化
比起访问remote memory,local memory 访问不仅延迟低(100ns),而且也减少了对公共总线(interconnect)的竞争。
合理地放置数据(比如直接调用NUMA api) , 软件调优化基本上还是围绕在尽量访问本地内存这一思路上。
如果本地内存已用完,那么尽量访问本CPU下相临节点的内存,避免访问跨CPU访问最远端的内存,通常可以提高20-30%性能。
4.CPU资源优化
CPU独占:独占CPU资源,减少调度影响,提高系统性能;
CPU绑定:减少CPU上下文切换,提高系统性能;
中断亲和 : 中断负载均衡,减轻其他CPU负担,提高系统性能;
进程亲和:减少CPU上下文切换,提高系统性能;
中断隔离:减少中断对CPU调度影响,提高系统性能;
5.内存优化
采用更大容量的内存,减少内存不足对性能影响,实现用空间换时间的性能优化;
采用大页内存,减少TLB misses,从而提高访存效率,如启用2M大页内存,甚至1G大页内存;
使用更新内存技术,比如DDR5,更好的内存硬件可以减少内存延迟,提高内存访问速度,从而提高系统性能。
6.锁和无锁设计优化
如何正确有效的保护共享数据是编写并行程序必须面临的一个难题,通常的手段就是同步。同步可分为阻塞型同步(Blocking Synchronization)和非阻塞型同步( Non-blocking Synchronization),多线程里面难免需要访问"共享内存",如果不加锁很容易导致结果异常,程序首先要保证正确,即使影响性能低也需要加锁来防止错误,此时该怎么提高CPU执行性能呢? 一个比较重要的优化工作是锁需要精心设计。
7.阻塞锁
阻塞锁通过改变了线程的运行状态。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的线程,通过竞争,进入运行状态;
mutex 主要用于线程间互斥访问资源场景;
semaphore 主要用于多个线程同步场景;
读写锁针主要用于读多写少场景;
8.非阻塞锁
非阻塞锁不会改变线程状态,使用时不会产生调度,通过CPU忙等待或者基于CAS(Compare - And - Swap)原子操作指令实现非阻塞访问资源;
自旋锁底层通过控制原子变量的值,让其他CPU忙等待,cache亲和性高和控制好锁粒度,可以提高多线程访问资源效率,主要用于加锁时间极短且无阻塞点场景;
RCU锁(Read-Copy Update)--非常重要一种无锁设计,对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它(因此不会导致锁竞争,不会导致锁竞争,内存延迟以及流水线停滞,读效率极高),但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作,RCU实际上是一种改进的读写锁,更能提高读多写少场景的系统性能;
原子操作可以保证指令以原子的方式执行(锁总线或者锁CPU缓存)——执行过程不被打断,主要用于全局统计、引用计数,无锁设计等场景;
CAS操作(Compare And Set或是 Compare And Swap),现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构,主要用于各种追求极限高性能场景,比如内存数据库,内存消息队列,DPDK的内存池mempool,java 的Disruptor等;
真正无锁-没有资源冲突,每个线程只使用local数据,最高级别的无锁设计,适合分而治之算法场景;
9.网络IO优化
零拷贝: 减少驱动到协议栈之间内存拷贝,减少用户空间到内核空间内存拷贝,提升IO性能;
DPDK:
kernelbypass:绕过内核协议栈(路径长,多核性能差),提高IO吞吐量;
PMD用户态驱动,使用无中断方式直接操作网卡的接收和发送队列;
采用HugePage减少TLB Miss;
DPDK采用向量SIMD指令优化性能;
CPU亲和性与独占;
Cache对齐,提高cache访问效率:
内存对齐:根据不同存储硬件的配置来优化程序,确保对象位于不同channel和rank的起始地址,这样能保证对象并并行加载,性能也能够得到极大的提升;
NUMA亲和,提高numa内存访问性能;
减少进程上下文切换:保证活跃进程数目不超过CPU个数;减少堵塞函数的调用,尽量采样无锁数据结构;
利用空间局部性,采用预取Prefetch,在数据被用到之前就将其调入缓存,增加缓存命中率;
充分挖掘网卡的潜能:借助网卡支持的分流(RSS、FDIR)和 卸载(TSO、CSUM)等特性;
10.预处理
预处理策略就是提前做好一些准备工作,这样可以提高后续处理性能;
如使用CPU预取指令对数据包进行预取,提前将所需要的数据取出来,可以提高流水线效率和缓存效率;
11.代码优化
循环优化:适当展开循环,可让指令并行执行,提供搞性能;
条件判断:减少条件判断语句,减少分支预测失败概率,提升CPU流水线效率,从而提升性能;
表达式优化: 优化布尔逻辑可以减少不必要计算;使++i 而不使用 i++可以减少中间临时变量;
采用位运算:如果没有越界风险,使用位运算符合计算机计算模型,效率更高;
内存&cache对齐/读写分离:数据结构最好是cache-line对齐的整数倍,同时数据结构的成员字段按读和写分开到不同的cache-line,高频访问的成员字段放到最前面,可提高cache命中效率,减少Cache miss;
指针优化:尽量减少指针使用,指针跳转会导致Cache miss;
向量化:合适使用SIMD高级指令可以优化代码;
inline优化:高频调用的处理逻辑尽可能 做到inline;
cache预取优化: 使用CPU预取指令对数据进行预取,提前将所需要的数据取出来,可以提高流水线效率和缓存效率;
插入其他语言:插入汇编,优化高频函数;
递归优化:尽量把递归修改为循环,减少递归调用代价;
12.惰性求值
惰性处理策略就是尽量将操作(比如计算),推迟到必需执行的时刻,这样很可能避免多余的操作。
中断后半部分优化,把可延迟函数放到延后处理,从而提高中断处理整体效率;
缺页中断处理,不需要进程把所有内存页载入内存,只有需要的时候再加载,这样可以减少大量无效内存操作,提高整体性能;
13.算法优化
算法复杂度优化:
O(1) < O(lgn) < O(n) < O(nlgn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n);
数据结构优化:hash结构 > 树型结构 > 线性结构;
14.编译优化
编译器优化:O0 -> O1 -> O2 -> O3,来额外的性能提升;
编译器API:使用内联函数,使用内存对齐API,使用cache对齐API等 ,可以更好让编译器优化代码,减少调用指令,提高性能;
四、参考文章
本文参考:Linux调度系统全景指南(终结篇)-宾大立