在Linux下做性能分析3:perf

==介绍==

ftrace的跟踪方法是一种总体跟踪法,换句话说,你统计了一个事件到下一个事件所有的时间长度,然后把它们放到时间轴上,你可以知道整个系统运行在时间轴上的分布。

这种方法很准确,但跟踪成本很高。所以,我们也需要一种抽样形态的跟踪方法。perf提供的就是这样的跟踪方法。

perf的原理是这样的:每隔一个固定的时间,就在CPU上(每个核上都有)产生一个中断,在中断上看看,当前是哪个pid,哪个函数,然后给对应的pid和函数加一个统计值,这样,我们就知道CPU有百分几的时间在某个pid,或者某个函数上了。这个原理图示如下:


很明显可以看出,这是一种采样的模式,我们预期,运行时间越多的函数,被时钟中断击中的机会越大,从而推测,那个函数(或者pid等)的CPU占用率就越高。

这种方式可以推广到各种事件,比如上一个博文我们介绍的ftrace的事件,你也可以在这个事件发生的时候上来冒个头,看看击中了谁,然后算出分布,我们就知道谁会引发特别多的那个事件了。

当然,如果某个进程运气特别好,它每次都刚好躲过你发起探测的位置,你的统计结果可能就完全是错的了。这是所有采样统计都有可能遇到的问题了。

还是用我们介绍ftrace时用到的那个sched_switch为例,我们可以用tracepoint作为探测点,每次内核调用这个函数的时候,就上来看看,到底谁引发了这个跟踪点(这个只能用来按pid分类,按函数分类没有用,因为tracepoint的位置是固定的),比如这样:

sudo perf top -e sched:sched_switch -s pid

当然,perf使用更多是CPU的PMU计数器,PMU计数器是大部分CPU都有的功能,它们可以用来统计比如L1 Cache失效的次数,分支预测失败的次数等。PMU可以在这些计数器的计数超过一个特定的值的时候产生一个中断,这个中断,我们可以用和时钟一样的方法,来抽样判断系统中哪个函数发生了最多的Cache失效,分支预测失效等。

下面是一个分支预测失效的跟踪命令和动态结果:

sudo perf top -e branch-misses


我们从这里就可以看到系统中哪些函数制造了最多的分支预测失败,我们可能就需要在那些函数中考虑一下有没有可能塞进去几个likely()/unlikely()这样的宏了。

而且读者应该也注意到了,perf比起ftrace来说,最大的好处是它可以直接跟踪到整个系统的所有程序(而不仅仅是内核),所以perf通常是我们分析的第一步,我们先看到整个系统的outline,然后才会进去看具体的调度,时延等问题。而且perf本身也告诉你调度是否正常了,比如内核调度子系统的函数占用率特别高,我们可能就知道我们需要分析一下调度过程了。

==使用perf==

perf的源代码就是Linux的源代码目录中,因为它在相当程度上和内核是关联的。它会使用Linux内核的头文件。但你编译内核的时候并不会编译它,你必须主动进入tools/perf目录下面,执行make才行。

perf支持很多功能,make的时候它会自动检查这些功能是否存在。比如前面我们用了tracepoint进行事件收集,你就要保证你的系统中有libtracepoint这个库。perf的自由度设计得相当高,很多功能你都可以没有,并不会影响你的基本功能。

由于perf和内核关联,所以理论上,你用哪个内核,就应该使用对应内核的perf,这能保证接口的一致。所以很多类似Ubuntu这样的发行版,你装哪个内核,就要装对应内核的perf命令,而通过的perf命令入其实只是个脚本,根据你当前的perf命令,调用不同perf版本。

但那只是理论上,实践中,其实perf的用户-内核接口相当稳定,很多时候跨版本使用是没有问题的,由于perf的版本还在高速发展中,而且很多发行版的perf版本没有使能很多功能,我在实践中经常直接找最新的内核自己重新编译版本,好像也没有出过什么问题。读者可以有限度参考这个经验。perf也没有很多的路径依赖,你编译完以后连安装都不用,直接用绝对路径调用你编译的版本即可。


==一般跟踪==

前面我们已经看了几个perf工作的例子了。类似git,docker等多功能工具,perf也是使用perf <子命令>这种模式。所有人首先需要学习的是两个最简单的命令:perf list和perf top。

perf list列出perf可以支持的所有事件。例如这样:


旧版本还会列出所有的tracepoint,但那个列表太长了,新版本已经不列这个东西了,读者可以直接到ftrace那边去看就好了。

perf top可以动态收集和更新统计列表,和很多其他perf命令一样。它支持很多参数,但我们关键要记住两个参数:

1. -e 指定跟踪的事件

-e可以指定前面perf list提供的所有事件(包括没有列出的tracepoint),可以用多个-e指定多个事件同时跟踪(但显示的时候会分开显示)

一个-e也可以直接指定多个事件,中间用逗号隔开即可:

sudo perf top -e branch-misses,cycles

(perf list给出的事件是厂家上传上去给Linux社区的,但有些厂家会有自己的事件统计,没有上传出去,这你需要从厂家的用户手册中获得,这种事件,可以直接用编号表示,比如格式是rXXXX,比如在我们的芯片里面,0x13号表示跨芯片内存访问,你就可以用-e r0013来跟踪软件的跨片访问次数)


事件可以指定后缀,比如我想只跟踪发生在用户态时产生的分支预测失败,我可以这样:

sudo perf top -e branch-misses:u,cycles

全部事件都有这个要求,我还可以:

sudo perf top -e ‘{branch-misses,cycles}:u'

看看perf-list的手册,会找到更多的后缀,后缀我也用得比较少,读者对这个有兴趣,可以自己深入挖掘一下,如果有什么好的使用经验,希望也可以告诉我。


2. -s 指定按什么参数来进行分类

-s参数可以不使用,默认会按函数进行分类,但如果你想按pid来分,就需要靠-s来进行分类了。前面我们已经看过这样的例子了。-s也可以指定多个域(用逗号隔开),例如这样:

sudo perf top -e 'cycles' -s comm,pid,dso

perf-top用来理解,体会perf的功能是比较好的,但实践中用得不多,用得比较多的是perf-record和perf-report命令。perf-record用来启动一次跟踪,而perf-report用来输出跟踪结果。

一般的过程是:

sudo perf record -e 'cycles' -- myapplication arg1 arg2
sudo perf report

下面是一个报告的例子:


perf record在当前目录产生一个perf.data文件(如果这个文件已经存在,旧的文件会被改名为perf.data.old),用来记录过程数据。之后运行的perf report命令会输出统计的结果。perf.data只包含原始数据,perf report需要访问本地的符号表,pid和进程的对应关系等信息来生成报告。所以perf.data不能直接拷贝到其他机器上用的。但你可以通过perf-archive命令把所有这些数据打包,这样移到另一个机器上就可以用了。

请注意,perf-archive是指perf-archive这个命令,不是指perf archive这个子命令。这个命令在编译perf源代码的时候会产生的,如果你的发行版不支持,可以自己编译一个。比较可惜的是,perf-archive备份的代码不能跨平台使用(比如你从arm平台上备份的数据,在x86上是分析不了的)。

perf.data保留前一个版本,可以支持perf diff这个命令,这个命令比较两次两次运行的区别。这样你可以用不同参数运行你的程序,看看运行结果有什么不同,用前面这个cs程序为例,我用4线程对比2线程,就有如下结果:

我们这里看到,增加线程后,heavy_cal的占比大幅下降了10.70%,其他的变化不大。

perf record不一定用于跟踪自己启动的进程,通过指定pid,可以直接跟踪固定的一组进程。另外,大家应该也注意到了,上面给出的跟踪都仅仅跟踪发生在特定pid的事件。但很多模型,比如一个webserver,你其实关心的是整个系统的性能,网络上会占掉一部分CPU,WebServer本身占一部分CPU,存储子系统也会占据部分的CPU,网络和存储不一定就属于你的WebServer这个pid。所以,对于全系统调优,我们常常给record命令加上-a参数,这样可以跟踪整个系统的性能。比如,还是前面这个cs程序的跟踪,如果我用-a命令去跟踪,得到的结果就和原来很不一样了:


大家注意一下Command那一列。那里已经不仅仅有cs这个进程了。

perf report是一个菜单接口,可以一直展开到每个函数的代码的,例如我们要展开上面这个heavy_cal()函数的具体计数,我们在上面回车,选择代码分析,我们可以得到:


perf record还有其他参数可以控制,例如可以通过-c指定事件的触发的事件次数等,那个读者们可以自己看手册。

和perf record/report类似的还有一个perf stat命令,这个命令不计算分布,仅仅进行统计,类似这样:


一般情况下,我觉得这个功能用不上。

==堆栈跟踪==

perf的跟踪有一个错觉需要我们注意,假设我们有一个函数abc(),调用另一个函数def(),在perf的统计中,这两者是分开统计的,就是说,执行def的时间,是不计算abc的时间的,图示如下:


这里,abc()被击中5次,def()被击中5次,ghi被击中1次。这会给我们不少错觉,似乎abc的计算压力不大,实际上不是,你要把def和ghi计算在内才行。

但这又带来另一个问题:可能def不仅仅是abc这个函数调用啊,别人也会调用它呢,这种情况,我们怎么知道是谁导致的?

这种情况我们可以启动堆栈跟踪,也就是每次击中的时候,向上回溯一下调用栈,让调用者也会被击中,这样就就更容易看出问题来,这个原理类似这样:


这种情况,abc击中了11次,def击中了6次,而ghi击中了1次。这样我们可以在一定程度上更容易判断瓶颈的位置。-g命令可以实现这样的跟踪,下面是一个例子:


使用堆栈跟踪后,start_thread上升到前面去了,因为正是它调的heavy_cal。

使用堆栈跟踪要注意的是,堆栈跟踪受扫描深度的限制,太深的堆栈可能回溯不过去,这是有可能影响结果的。

另一个问题是,有些我们从源代码看来是函数调用的,其实在汇编一级并不是函数调用。比如inline函数,宏,都不是函数调用。另外,gcc在很多平台中,会自动把很短的函数变成inline函数,这也不产生函数调用。还有一种是,fastcall函数,通过寄存器传递参数,不会产生调用栈,也有可能不产生调用栈,这个通过调用栈回溯是有可能看不到的。

还有一种更奇葩的情况是,部分平台使用简化的堆栈回溯机制,在堆栈中看见一个地址像是代码段的地址,就认为是调用栈,这些情况都会引起堆栈跟踪上的严重错误。使用者应该对系统的ABI非常熟悉,才能很好驾驭堆栈跟踪这个功能的。


==其他功能==

perf是现在Linux中主推的性能分析工具,几乎每次升级都会有重大更新,连什么benchmarking的功能都做进来了,还有用于专项分析perf-mem这样的命令,用来产生脚本的perf script命令,帮助你用不同的脚本语言分析操作结果。这个用户可以自己看手册去,有前面的基础,这些功能都是很好理解的。

不过特别提一下script命令,虽然它的功能看起来只是用来产生分析脚本的,但我们还常常用来导出原始分析数据,读者可以在perf-record后直接用这个命令来导出结果:

sudo perf script


这里列出每个击中点,你爱怎么处理这些击中点的数据,就全凭你的想象力了。


==perf跟踪的缺陷==

前面已经强调过了,perf跟踪是一种采样跟踪,所以我们必须非常小心采样跟踪本身的问题,一旦模型设置不好,整个分析结果可能都是错的。我们要时刻做好这种准备。

我特别提醒的是,你每次看perf report的报告,首先要去注意一下总共收集了多少个点,如果你只有几十个点,你这个报告就可能很不可信了。

另外,我们要清楚,现代CPU基本上已经不用忙等的方式进入等待了,所以,如果CPU在idle(就是没有任务调度,这种情况只要你的CPU占用率不是100%,必然要发生的),击中任务也会停止,所以,在Idle上是没有点的(你看到Idle函数本身的点并非CPU Idle的点,而是准备进入Idle前后花的时间),所以,perf的统计不能用来让你分析CPU占用率的。ftrace和top等工具才能看CPU占用率,perf是不行的。

perf还有一个问题是对中断的要求,perf很多事件都依赖中断,但Linux内核是可以关中断的,关中断以后,你就无法击中关中断的点了,你的中断会被延迟到开中断的时候,所以,在这样的平台上,你会看到很多开中断之后的函数被密集击中。但它们是无辜的。但更糟糕的是,如果在关中断的时候,发生了多个事件,由于中断控制器会合并相同的中断,你就会失去多次事件,让你的统计发生错误。

现代的Intel平台,基本上已经把PMU中断都切换为NMI中断了(不可屏蔽),所以前面这个问题不存在。但在大部分ARM/ARM64平台上,这个问题都没有解决,所以看这种平台的报告,都要特别小心,特别是你看到_raw_spin_unlock()一类的函数击中极高,你就要怀疑一下你的测试结果了(注意,这个结果也是能用的,只是看你怎么用)。


==小结==

这一篇我们介绍了perf的基本用法,perf通常是我们进行性能分析的第一步,但这一步,要用好也不是那么容易的,我们首先应该掌握它的原理,然后基于一个分析模型逐步用perf来验证我们的猜测,我们才有可能真正发现问题。

编辑于 2018-07-05

文章被以下专栏收录