Python性能分析与优化(一) —— 性能分析器

Python性能分析与优化(一) —— 性能分析器

今天读了Python性能分析与优化这本书大半了,开始写几篇博客,首先介绍一下这本书吧。Fernando Doglio写的这本书包含8个章节,其中第二章性能分析器介绍了几种关于Python性能分析的工具;第三章可视化分析数据介绍了几种GUI的性能分析工具;第四章优化细节介绍了一些优化性能的技巧。今天先从第二章开始吧。

cProfile

Python呢,自带了一个cProfile的包,它是用于测量CPU时间、统计函数调用次数的一个包。所以cProfile不需要安装,只需要在程序中导入cProfile的包即可。

首先我们来简单测试一下cProfile包的使用效果:

上面是一段简单的测试代码,其中我们创建了一个Profile类用于记录性能情况,用Stats类输出统计数据。这个print_stats函数接收了三个参数,第一个10表示打印前10行,第二个1.0表示按总行数的100%打印,第三个是正则表达式,用于匹配stdname。

对应的输出情况如下图所示:

  • 其中第一行告诉我们一共有196个函数调用被监控,其中191个是原生调用,表明这些调用不涉及递归。

  • ncalls表示函数调用的次数。如果在这一列中有两个数值,则表明有递归调用,第二个数值是原生调用的次数,第一个数值是总调用次数。如果第一个数值很大则表明需要进行内联函数扩展。

  • tottime表示函数内部消耗的总时间,这个性能信息可以帮助我们找到需要优化的点。

  • percall是tottime/ncalls的结果,表示一个函数每次调用的平均消耗时间。

  • cumtime是之前所有子函数消耗时间的累积和。这个数值可以帮助开发者从整体视角识别性能问题,比如算法选择错误。

  • 另一个percall是用cumtime除以原生调用的数量,表示到该函数调用时,每个原生调用的平均消耗时间。

斐波那契数列性能

好了让我们实战一下cProfile性能分析器吧。这次我们选择用斐波那契数列生成函数进行示范。

首先是递归实现斐波那契数列的核心代码:

以及主函数:

用python执行脚本结果如下:

其实这是因为递归执行fib函数次数太多了,导致栈空间不足,因此我们修改一下程序,非递归执行(即采用迭代的方法获取斐波那契数列),得到新的版本如下:

采用迭代的方法执行python程序,性能上是怎样呢?

我们可以看到,fib这个函数被调用了5005次,占用的时间最多,达到了0.308s,还有0.015s用在了fib函数中的循环中,我们知道,因为在求斐波那契数列时,fib(1)是重复多次被调用的,于是我们可以使用缓存的方式来存储我们求过的斐波那契数据。代码如下:

运行一下,看下结果如何:

这下我们看到了,cached_fib函数只被调用了1001次,占用0.069s,也就是说,节省了4004次重新计算斐波那契数据。

总结

cProfile作为python自带的性能分析器,其优点是简单方便,能看到CPU的时间占用情况以及函数调用情况。有丰富的API用于执行、输出等。

但是其缺点是无法统计内存等情况,并且误差存在且不可忽视,因为cProfile统计时间利用的是系统内部的时钟,由于切换函数等,存在一定的滞后,所以误差不可避免。

line_profiler

接下来是一款逐行分析的性能分析器——line_profiler。它试图弥补cProfile和类似性能分析器的不足。其他性能分析器主要关注函数调用消耗的CPU时间。大多数情况下,这足以发现问题,消除瓶颈。但是,如果瓶颈问题出现在函数中的某一行,这是就需要line_profiler了。

安装

使用pip安装line_profiler:

1
pip install line_profiler

如果安装过程中出现问题,比如文件缺失,则可以安装如下依赖

1
2
> sudo apt-get install python-dev libxml2-dev libxslt-dev
>

斐波那契数列性能

下面用原来的代码继续分析斐波那契数列生成函数。这里的代码只需要更改一小部分的接口即可,其他的核心函数不用变。

其结果如下,依然是递归调用次数过多,栈空间不足导致程序崩溃。

当我们将其改为非递归(迭代执行)时,分析程序每一行的性能:

小结

相比cProfile,line_profiler能够分析到每个函数的每一行的执行次数及时间等,但是缺点也很明显,line_profiler不能够从整体程序上进行分析,并且也没有针对内存等关键性能进行分析。所以line_profiler适合用于模块性能的分析。