Python性能分析(2)-line_profiler

本文最后更新于 2023年3月6日 下午

简介

line_profiler 包提供了对函数运行时间逐行分析的功能。原作者为 Robert Kern,目前由社区组织 OpenPyUtils 维护更新。

本文大部分内容来自于对 line_profiler 文档的中文翻译,旨在向更多中文用户推广该工具。

快速上手

使用 line_profiler 分析函数性能只需要三步:

  1. 通过pip安装:pip install line_profiler
  2. 在需要被分析的函数上添加 @profile 装饰器
  3. 运行 kernprof -lv script_to_profile.py

安装

可以使用 pip 安装 line_profiler 的发行版:

1
$ pip install line_profiler

也可以通过 pip 安装适用于 IPython 的版本(注意版本间的兼容问题):

1
$ pip install line_profiler[ipython]

可以使用 Git 查看开发源码:

1
$ git clone https://github.com/pyutils/line_profiler.git

line_profiler

当前 Python 中支持的分析工具只计量函数调用时间。这是在程序中定位热点的良好开始,经常也是优化程序所需要做的全部工作。然而,有时热点仅仅来自于函数中的某一行,只依靠阅读源码很难发现。这类情况在科学计算中尤为常见。科学计算中的函数往往更大(因为合理算法的复杂性,或因为程序员仍然在尝试编写 FORTRAN 代码),以及在函数体之外的使用 numpy 等库的单个语句会触发大量计算。cProfile 只计时显式函数调用,而非因语法调用的特殊方法。因此,像这样的在大型数组上使用相对较慢的 numpy 操作,

1
a[large_index_array] = some_other_large_array

是一个永远不会被 cProfile 打破的 hotspot,因为该语句中并没有显式函数调用。

向 LineProfiler 中传入函数以进行分析,它将会计量这些函数中每一行执行的耗时。在典型工作流中,人们只关心个别几个函数的行时间,因为梳理每一行代码的计时结果会让人不知所措。而 LineProfiler 的确需要被明确告知要对哪些函数进行分析。最简单的方法是使用 kernprof 脚本。

1
$ kernprof -l script_to_profile.py

kernprof 将创建一个名为 profile 的 LineProfiler 实例,并将其插入 __builtins__ 命名空间。它被当作装饰器使用,所以在你的脚本中,使用 @profile 来装饰想要分析的函数。

1
2
3
@profile
def slow_function(a, b, c):
...

kernprof 的默认行为是将结果存储到二进制文件 script_to_profile.py.lprof 中。可以使用 [-v/--view] 选项告知 kernprof 立即在终端显示格式化的结果。或者,可以像这样稍后查看结果:

1
$ python -m line_profiler script_to_profile.py.lprof

例如,下面是对带有装饰器版本的 pystone.py benchmark 中的一个函数进行分析的结果(前两行是 pystone.py 的输出,而非 kernprof):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Pystone(1.1) time for 50000 passes = 2.48
This machine benchmarks at 20161.3 pystones/second
Wrote profile results to pystone.py.lprof
Timer unit: 1e-06 s

File: pystone.py
Function: Proc2 at line 149
Total time: 0.606656 s

Line # Hits Time Per Hit % Time Line Contents
==============================================================
149 @profile
150 def Proc2(IntParIO):
151 50000 82003 1.6 13.5 IntLoc = IntParIO + 10
152 50000 63162 1.3 10.4 while 1:
153 50000 69065 1.4 11.4 if Char1Glob == 'A':
154 50000 66354 1.3 10.9 IntLoc = IntLoc - 1
155 50000 67263 1.3 11.1 IntParIO = IntLoc - IntGlob
156 50000 65494 1.3 10.8 EnumLoc = Ident1
157 50000 68001 1.4 11.2 if EnumLoc == Ident1:
158 50000 63739 1.3 10.5 break
159 50000 61575 1.2 10.1 return IntParIO

逐行显示出了函数的源码及其耗时信息。共有六列信息。

  • Line #: 在文件中的行号
  • Hits: 该行被执行的次数
  • Time: 执行该行的总时间,以定时器为单位表示。在表格前的标题信息中,有一行“Timer unit(定时器单位)”,给出了与秒的换算关系。在不同的系统上可能有所不同。
  • Per Hit: 以定时器为单位,执行一次该行的平均时间
  • % Time: 该行所花费的时间,在该函数的总记录时间中所占的百分比
  • Line Contents: 实际的源码。注意这是在查看格式化结果时从磁盘上读取的,而不是在执行时获取。如果在执行测试与格式化查看之间编辑了源码文件,这些行就会不一致,格式化器甚至可能无法找到要显示的函数

kernprof

kernprof 也可用于 cProfile、其第三方实现 Isprof 或纯 Python profile 模块。它有如下几个特性:

  • 对性能分析进行封装。无需为了启动分析与保存结果而修改原有的脚本。当然,想要使用更高级的 __builtins__ 功能除外。
  • 高鲁棒性的脚本运行能力。许多脚本需要使用 __name____file__sys.path 等来处理相对路径。如果简单地只使用 execfile() 来进行封装,则许多依赖这些信息的脚本会失效。kernprof 会在执行脚本前正确处理这些变量。
  • 简单的可执行程序位置。如果要对安装在 PATH 上的应用程序进行性能分析,可以直接使用其名称。如果 kernprof 在当前目录下没有找到指定的脚本,则会在 PATH 中搜索。
  • 将性能分析器插入到 __builtins__ 中。有时只想对代码的一小部分进行分析,则可以使用 [-b/--builtin] 选项,分析器将以 “profile” 的名称被实例化并插入到 __builtins__ 中。像 LineProfiler 一样,它可以作为装饰器使用,或者通过 enable_by_count() 和 disable_by_count() 启用/禁用,甚至可以通过 “with profile:” 语句作为上下文管理器使用
  • 设置分析准备。使用 [-s/--setup] 选项,可以在执行主脚本前,以不做分析的方式运行一个指定脚本。这通常适用于像 wxPython 或 VTK 这样的大型库的导入对结果产生干扰的情况。如果可以修改源码,使用 __builtins__ 的方式可能更容易。

分析 script_to_profile.py 的结果默认写入 script_to_profile.py.prof 中。它是一个典型的 marshalled 文件,可以用 pstats.Starts() 读取。可以用命令进行交互式查看:

1
$ python -m pstats script_to_profile.py.prof

这些文件也可以用图形化工具来查看。基于 cProfileline_profiler 的第三方工具列表如下:


Python性能分析(2)-line_profiler
https://muzing.top/posts/bf8d8cfe/
作者
Muzing
发布于
2023年2月1日
更新于
2023年3月6日
许可协议