Python 性能分析器入门教程
在 Python 编程过程中,性能分析是一个至关重要的环节。性能分析可以帮助我们找出程序中的瓶颈,从而优化代码,提高程序的运行效率。Python 标准库提供了 cProfile
和 profile
模块来实现确定性性能分析。本文将深入浅出地为您介绍这两个模块的使用方法,让您轻松掌握 Python 性能分析技巧。
确定性性能分析 是指监控程序中所有的函数调用、函数返回和异常事件,并精确计时这些事件之间的时间间隔。与统计分析相比,确定性分析能够提供更详细、准确的运行时统计信息,帮助我们更好地了解程序的性能状况。
一、cProfile
和 profile
模块简介
Python 提供了两种性能分析模块:
cProfile
:这是一个 C 扩展插件,具有较低的运行开销,适合分析长时间运行的程序。它基于lsprof
开发,是大多数用户的首选。profile
:这是一个纯 Python 模块,虽然运行开销较大,但更易于扩展和定制。如果需要对分析器进行深入的二次开发,这个模块会更加合适。
通常情况下,我们推荐使用 cProfile
模块,因为它在性能分析过程中对程序运行的影响较小。
二、性能分析基础操作
1. 分析单个函数
以下是使用 cProfile
模块分析单个函数的简单示例:
import cProfile
import re
cProfile.run('re.compile("foo|bar")')
执行上述代码后,将输出类似于以下的分析结果:
214 function calls (207 primitive calls) in 0.002 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.002 0.002 {built-in method builtins.exec}
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 __init__.py:250(compile)
1 0.000 0.000 0.001 0.001 __init__.py:289(_compile)
1 0.000 0.000 0.000 0.000 _compiler.py:759(compile)
1 0.000 0.000 0.000 0.000 _parser.py:937(parse)
1 0.000 0.000 0.000 0.000 _compiler.py:598(_code)
1 0.000 0.000 0.000 0.000 _parser.py:435(_parse_sub)
结果解读 :
-
第一行显示共有 214 次函数调用,其中 207 次为原始调用(非递归调用)。
-
第二行表示输出结果是按照累计时间(cumulative time)排序的。
-
各列含义如下:
ncalls
:函数调用次数。tottime
:在该函数中执行所花费的总时间(不包括调用子函数的时间)。percall
:tottime
与ncalls
的比值,表示每次调用该函数的平均时间。cumtime
:该函数及其所有子函数的累计执行时间。percall
:cumtime
与原始调用次数的比值。filename:lineno(function)
:函数所在的文件名、行号和函数名。
2. 保存分析结果
如果不想直接打印分析结果,可以将其保存到文件中,以便后续分析:
import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')
此时,分析结果会被保存到名为 restats
的文件中。之后可以使用 pstats.Stats
类来读取和处理这些结果。
3. 命令行分析
cProfile
和 profile
模块还可以作为脚本直接调用,用于分析其他脚本的性能。例如:
python -m cProfile [-o output_file] [-s sort_order] (-m module | myscript.py)
-o output_file
:将性能分析结果保存到指定文件中,而不是输出到标准输出。-s sort_order
:指定排序方式,例如按时间排序(time
)、按累计时间排序(cumulative
)等。-m module
:指定要分析的模块而不是脚本。
三、pstats.Stats
类的使用
pstats.Stats
类提供了丰富的功能,用于分析和格式化性能分析结果。
1. 基本读取和打印
import pstats
from pstats import SortKey
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()
strip_dirs()
:从函数文件名中移除多余的路径信息,使输出更简洁。sort_stats(-1)
:对分析结果进行排序,-1
表示按标准名称排序。print_stats()
:打印分析结果。
2. 按不同条件排序
可以按不同的条件对分析结果进行排序,以便更直观地了解程序性能。
p.sort_stats(SortKey.NAME)
p.print_stats()
此代码按函数名称排序并打印结果。
p.sort_stats(SortKey.CUMULATIVE).print_stats(10)
此处按累计时间排序,并打印前 10 行结果,有助于快速定位程序中的性能瓶颈。
p.sort_stats(SortKey.TIME).print_stats(10)
按每个函数自身的执行时间排序,同样显示前 10 行。
3. 筛选和查找
可以根据特定条件筛选分析结果,例如:
p.sort_stats(SortKey.FILENAME).print_stats('__init__')
按文件名排序,并打印所有包含 __init__
的函数的分析结果。
p.sort_stats(SortKey.TIME, SortKey.CUMULATIVE).print_stats(.5, 'init')
先按时间排序,再按累计时间排序,打印出原始结果 50% 的数据中包含 init
的相关函数信息。
还可以查看函数的调用者和被调用者:
p.print_callers(.5, 'init')
显示调用了包含 init
的函数的调用者列表。
p.print_callees()
打印被指定函数调用的所有函数列表。
四、profile.Profile
类的使用
在需要更精确地控制性能分析过程时,可以直接使用 profile.Profile
类。
1. 基本性能分析
import cProfile, pstats, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
# 在这里执行要分析的代码
pr.disable()
s = io.StringIO()
sortby = SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())
enable()
:开始收集性能分析数据。disable()
:停止收集数据。
也可以使用上下文管理器来简化代码:
import cProfile
with cProfile.Profile() as pr:
# 在这里执行代码
pr.print_stats()
2. 其他方法
profile.Profile
类还提供了以下方法:
create_stats()
:停止收集数据,并将结果记录为当前 profile。print_stats(sort=-1)
:根据当前性能分析数据创建一个Stats
对象并打印结果,sort
参数用于指定排序方式。dump_stats(filename)
:将性能分析结果写入指定文件。run(cmd)
:对指定命令进行性能分析。runctx(cmd, globals, locals)
:在指定的全局和局部环境下对命令进行性能分析。runcall(func, /, *args, **kwargs)
:对函数调用进行性能分析。
五、性能分析结果解读与应用
通过性能分析得到的数据,我们可以深入理解程序的运行情况,从而进行有针对性的优化。
1. 识别性能瓶颈
重点关注 tottime
和 cumtime
较高的函数,这些函数可能是程序的性能瓶颈。例如,如果某个函数的 tottime
很高,说明该函数自身的执行效率较低,可能需要优化其算法或实现方式;如果 cumtime
较高而 tottime
相对较低,则表明该函数调用了许多其他耗时函数,可能需要对整体的函数调用结构进行优化。
2. 优化函数调用
根据 ncalls
列,找出调用次数过多的函数。如果一个函数被频繁调用,但其实可以合并或减少调用次数,那么优化这部分代码将对程序性能产生显著提升。例如,将一些重复的计算移到循环外部,或者使用更高效的数据结构来减少不必要的函数调用。
3. 分析递归函数
对于递归函数,性能分析结果中的 ncalls
列可能会显示多个调用次数(例如显示为 3/1
),其中第一个数字是总调用次数,第二个数字是原始调用次数。通过这种方式,我们可以了解递归函数的调用深度和频率,从而判断是否需要对递归实现进行优化,或者考虑使用迭代代替递归以提高性能。
4. 持续监测与迭代优化
性能分析不是一劳永逸的工作,而是一个持续的过程。在对程序进行优化后,再次进行性能分析,对比优化前后的数据,评估优化效果。根据新的分析结果,继续寻找潜在的性能瓶颈并进行优化,通过不断的迭代,逐步提升程序的性能表现。
六、自定义计时器与准确估量
1. 自定义计时器
如果需要改变时间测量方式,可以向 Profile
类构造器传入自定义的计时函数:
import time
pr = profile.Profile(time.perf_counter)
- 对于
profile.Profile
,计时函数可以返回一个数字或数字列表。 - 对于
cProfile.Profile
,计时函数应返回一个数字,如果返回整数,还可以通过第二个参数指定单位时间长度。
2. 准确估量
为了提高性能分析的准确性,可以对 profile
模块的性能分析器进行校准:
import profile
pr = profile.Profile()
for i in range(5):
print(pr.calibrate(10000))
校准后,可以通过以下方式应用计算出的偏差值:
# 方法 1:应用于所有后续创建的 Profile 实例
profile.Profile.bias = your_computed_bias
# 方法 2:应用于特定的 Profile 实例
pr = profile.Profile()
pr.bias = your_computed_bias
# 方法 3:在构造函数中指定
pr = profile.Profile(bias=your_computed_bias)
七、总结
掌握 Python 的 cProfile
和 profile
模块,能够让我们轻松地对程序进行性能分析,找出性能瓶颈并进行优化。在编程狮平台上,您可以进一步学习和实践这些性能分析工具,提升自己的 Python 编程技能,编写出更高效、更优质的代码。通过持续的性能分析和优化,让您的程序在各种场景下都能表现出色。
更多建议: