Python代码性能分析(后篇)
前言
在前一篇文章中,我介绍了测量执行Python代码所需要的总体时间的几种方法。而在实际的性能分析场景中,目标代码的逻辑往往比较复杂,光靠总体执行耗时并不能帮助我们快速定位性能瓶颈。这个时候就需要请出Python的标准库:cProfile
(官方文档)来对代码进行细致的性能分析了。
和前一篇文章一样,本文所使用的测试平台为Ubuntu 18.04
+Python 3.6
,并使用代码文件slow_func.py
作为样例进行性能分析,内容如下:
1 |
|
命令行使用cProfile
对于单独的Python代码文件来说,通过命令行使用cProfile无疑是最方便的选择。
执行如下shell命令:
1 |
|
首先用
python3
的-m
选项调用cProfile
模块,然后用cProfile
的-s
选项让输出结果按tottime
进行排序,最后执行slow_func.py
文件。
完整调用格式为:python -m cProfile [-o output_file] [-s sort_order] myscript.py
得到如下结果:
1 |
|
- 输出的第1行表明,脚本文件执行中存在1600多万次的函数调用,共耗费3.268秒。
- 而根据前一篇文章的测试结果,直接执行该脚本文件只需要2秒左右的时间,那多出来的1秒多花在了哪里?这是因为
cProfile
需要对每一次函数调用进行监控和记录,由于该文件存在较多的函数调用,所以总执行耗时也就增长了许多了。
- 而根据前一篇文章的测试结果,直接执行该脚本文件只需要2秒左右的时间,那多出来的1秒多花在了哪里?这是因为
- 第3行表明,下表内容按照
internal time
(内部执行时间,tottime
)排序,这是由执行命令中的-s tottime
参数决定的。 - 第5行为分析结果表的表头,依次为
ncalls
:调用次数、tottime
:内部执行耗时、percall
:内部执行耗时/调用次数、cumtime
:累计执行耗时、percall
:累计执行耗时/调用次数,以及最后的文件名+行号+函数名称
。tottime
和cumtime
的区别在于,tottime
不包括子函数执行所花费的时间,而cumtime
是包括的。
- 第6行表明,
slow_func.py
中第8行的func2
函数共执行了1次,内部耗时1.456秒,累计耗时2.172秒。 - 第7行表明,Python内置的
sleep
函数共执行了一次,耗时1.001秒。 - 第8行表明,Python内置的
random
函数共执行了1600多万次,耗时0.716秒。- 由于
random
函数是被func2
函数调用的,所以这0.716秒和func2
函数的内部执行耗时1.456秒,共同组成了func2
函数的累计执行耗时:2.172秒。
- 由于
- 由于是用
cProfile
分析整个脚本文件,所以许多Python自身所需的函数调用也被展示在了结果里,所以分析结果表才会有100多行的规模。这个问题可以在下一小节中解决。
代码里使用cProfile
从本质上来说,通过命令行使用cProfile
相当于在代码里使用cProfile
的一个简化操作。而在命令行里分析代码有着明显的局限性:目标代码必须独立成文件、输出格式固定等等。所以,在代码里使用cProfile
往往是一个更优的选择。
执行如下Python代码:
1 |
|
代码的核心逻辑是使用
cProfile
模块的Profile
类对代码块进行性能分析,分析完成后使用pstats
模块的Stats
类将分析结果按一定格式写入至内存文件,最后输出该文件里写入的内容。
实际上这只是一个较为简单的样例,pstats
模块还可以获得函数之间的调用关系、将结果持久化、显示文件路径等等,更完整的说明可以参考官方文档。
得到如下结果:
1 |
|
cProfile结果可视化
一般来说,通过以上两个例子就可以获得完善的性能分析报告了。但通过一些可视化工具对cProfile
的报告进行二次处理,我们可以更清晰地观察函数之间的调用关系、更轻松地找出性能瓶颈,算是一个不错的辅助手段。在这里只介绍一种可视化工具:JetBrain PyCharm自带的Profile
工具。
点击Pycharm中
Run
菜单里的Profile 'xxx'项目
,即可对当前运行执行方案使用cProfile
进行性能分析,如下图:结果如下图所示。其中,
Time
对应cProfile
中的cumtime
,即累计执行耗时;Own Time
对应cProfile
中的tottime
,即内部执行耗时。