很多软件都要做性能分析和性能优化。很多语言都会有他的性能分析工具,例如如果优化C++的性能,我们可以用Visual Studio自带的性能探测器,或者使用Intel VTune Profiler。了解性能分析工具的原理有助于了解工具给出的数据与结果,也能帮助我们在遇到异常结果时排查哪里出了问题。这篇博客简单总结一下常见的性能分析工具原理。
性能分析器原理分类
性能分析工具大部分都可以分为下面几类
- 基于采样(Sampling)
- 基于插装(Instrumentation)
- 基于事件(Event-based)
1. 基于采样
基于采样的分析器会每隔一个固定时间间隔暂停所有线程的执行,然后分析有哪些线程正在执行,那些线程处于就绪状态。对于这类线程,我们记录每个线程的调用堆栈,以及其他需要的信息。我们称这个行为为一次采样,记录下来的每个堆栈为一个样本。然后在结束性能分析的时候我们统计记录下载的堆栈,看每个堆栈被记录了多少次,或者每个函数被记录了多少次。统计学告诉我们,那些执行时间比较长的函数、堆栈,被记录的次数会更多。如果堆栈A被记录了200次,堆栈B被记录了100次,那么堆栈B的执行时间是堆栈A的2倍。我们可以计算某个堆栈样本的数量占总样本数的比例,这个比例就是堆栈执行时间的占比。用Visual Studio的性能探测器我们看到的百分比和数字就是值样本的占比(也就是时间占比)和样本次数。
很多性能分析工具都是基于采样的方式。运行性能分析器是会影响被测程序的性能的,而基于采样的有点是对性能影响比较小,不需要重新编译程序,非常方便。
2.基于插装
插装是指通过修改程序,往里面加入性能分析相关代码,来收集、监控相关性能指标。例如我们可以在一个函数的开头写下计数代码,我们就可以知道在运行中这个函数被执行了多少次。一般来说基于插装的性能分析更为准确,但是对性能影响比较大。因为需要修改代码,所以也不是很方便。另外,基于插装的分析也可能会引起海森堡bug(heisenbug)。海森堡bug是指当你再运行程序的时候能遇到这个bug,但是试图定位这个bug时却遇不到这个bug。这个bug往往是因为在定位bug时修改了软件的运行环境/流程,导致软件执行和生产时不一样,于是就遇不到这个bug了。这个命名的来源很有意思,海森堡是量子力学的著名科学家,他提出了不确定性原理,以及观察者理论。这个理论认为,观察会影响例子的状态,导致观察粒子和不观察粒子会导致不同的结果,这个和海森堡bug的情形非常相似。关于观察者理论,有兴趣的人可以再了解一下。
回到正题,基于插装也可以再进行划分:
- 人手修改源码:这个是我们非常常用的性能分析方法。我们做性能分析有时候就会直接修改源码,计算某一段代码的执行时长,或者计算函数调用次数,分析哪段代码耗时。
- 工具自动修改源码
- 工具自动修改汇编/中间码
- 运行时注入
- ......
3.基于事件
在软件执行过程中,可能会抛出某些事件。我们通过统计事件出现的次数,事件出现的时机,可以得到软件的某些性能指标。事件又可以分为软件事件和硬件事件。软件事件比如Java可以在class-load的时候抛出事件。硬件事件则是使用专门的性能分析硬件。现在很多CPU里面都有用于分析性能的性能监控单元(PMU),PMU本身是一个寄存器,在某个事件发生时寄存器里面的值会+1。例如我们可以设置为当运行中发生memory cache miss的时候PMU寄存器+1,这样我们就知道一段时间内发生了多少次memory cache miss。性能分析器使用PMU时,会给PMU设置需要统计的事件类型,以及Sample After Value (SAV)。SAV是指寄存器达到什么值之后出发硬件中断。例如,我们可以给PMU设置SAV为2000,统计事件为memory cache miss。那么当cache miss发生2000次时,发生硬件中断。这个时候性能分析器就可以收集CPU的IP,调用堆栈,进程ID等等信息,分析器结束时进行统计分析。
基于硬件事件的优点是,对被测程序的影响非常小,比基于采样还小,可以使用更短的时间间隔收集信息,而且可以收集CPU的非常重要的性能指标,例如cache miss,分支预测错误,等等。
但是基于硬件事件的分析器也有它的一些问题,导致数据上的误差:
-
SAV一般会设置成很大的数值:
像是Intel VTune Profiler一般会把SAV设置成10,000到2,000,000,发生中断时我们能知道最后一次触发该事件是哪段代码引起的,但是在这之前的9,999到1,999,999次事件我们是不知道的。他会认为所有10,000到2,000,000次事件都是由同一处代码引起的。如果发生了很多次中断,收集了很多次信息,而某一行代码出现了很多次,那么根据统计学,我们能知道这行代码触发了多少次事件。但是如果某一行代码只出现了一两次,那么我们很难知道这行代码到底出发了多少次时间。 -
CPU一个核只有几个PMU,但是可以统计的事件有几十种:
一个PMU可以统计一个事件,但是我们分析一个软件可能需要统计几十种事件。一般的处理方法是多路复用(Multiplexing)。比如说前10ms记录事件A,后10ms记录事件B,再后10ms由记录事件A,……,这样轮流记录事件A和事件B,那么一个PMU就可以同时统计两个事件。多路复用可以解决少量PMU统计大量事件的问题,但是也会导致每种事件记录的样本数减少,倒是最后统计计算的结果误差更大。 -
Intel® Hyper-Threading Technology导致记录不准:
Hyper-Threading技术可以让一个CPU物理核变成两个逻辑核,但是这两个逻辑核会共享很多硬件,包括PMU。这会出现什么问题呢?例如我们有两个线程再两个CPU核同时运行。我们觉得实在两个核上执行,但是实际上是在同一个物理核上。所以PMU会同时统计两个程序触发的事件,我们很难区分到底是哪个逻辑核出发了多少事件,所以PMU记录的数据就会不准确。另外,性能分析器计算性能指标时会使用一些硬件参数,Hyper-Threading技术会让这些参数不准确。例如一般个CPU核能再一个clock执行4个uop,所以CPI(Cycle Per Instruction,每个clock执行的指令数)是0.25。但是如果使用了Hyper-Threading技术,实际的CPI会是0.5 -
Event Skid(事件打滑)会导致记录的IP不准确:
PMU记录有些事件会出现一定延时,所以在执行分析器的中断处理代码时,可能被测程序已经又执行了好多指令,IP已经向后滑动了很远了。一般如果我们只是统计函数的话不会太大问题,但是我们想统计指令时就会有很大问题了。比如我们可能会看到某个add指令导致了大量的分支预测错误,显然这个是不可能的。往往这种时候我们可以看看前面一点的指令。 -
Interrupt Masking(中断屏蔽),导致统计出来的空闲状态比正常的高
不同的中断有不同的优先级,为了高优先级的中断处理程序不被打断,我们可以选择屏蔽一部分中断事件。但是PMU的事件也是一个中断,如果系统中有大量的中断屏蔽,那么会有PMU的中断被屏蔽掉一部分,导致统计出来的数据不准确。表现出来的效果就是,统计出来的处于空闲状态的时间比实际的要高。
总结
这几个就是比较常见的性能分析方法。我们知道了性能分析的原理,可以更好地理解性能分析器给出的结果,也可以在出现明显异常的结果时,分析判断可能的原因,针对性的解决。
参考
https://en.wikipedia.org/wiki/Profiling_(computer_programming)
https://software.intel.com/content/www/us/en/develop/articles/understanding-how-general-exploration-works-in-intel-vtune-amplifier-xe.html
https://software.intel.com/content/www/us/en/develop/documentation/vtune-help/top/analyze-performance/user-mode-sampling-and-tracing-collection.html
https://software.intel.com/content/www/us/en/develop/documentation/vtune-help/top/analyze-performance/hardware-event-based-sampling-collection.html