26.4. Python 探查器

源代码: Lib/profile.pyLib/pstats.py


26.4.1. 探查器简介

cProfileprofile提供 Python 程序的“确定性分析”。 * profile *是一组统计信息,描述了程序的各个部分执行的频率和时间。这些统计信息可以passpstats模块格式化为报告。

Python 标准库提供了同一概要分析接口的三种不同实现:

  • 建议大多数用户使用cProfile;它是具有合理开销的 C 扩展,使其适合于分析长时间运行的程序。基于lsprof,由 Brett Rosen 和 Ted Czotter 贡献。

2.5 版的新Function。

  • profile,一个纯 Python 模块,其接口由cProfile模仿,但是会增加概要分析程序的开销。如果您try以某种方式扩展探查器,则使用此模块可能会更轻松。由 Jim Roskind 最初设计和编写。

在版本 2.4 中进行了更改:现在还报告了调用内置函数和方法所花费的时间。

  • hotshot是一个实验性的 C 模块,该模块专注于最大程度地减少性能分析开销,但要花费更长的数据后处理时间。它不再维护,并且可能在将来的 Python 版本中删除。

在版本 2.5 中进行了更改:结果应该比过去更有意义:计时核心包含一个严重的错误。

profilecProfile模块导出相同的接口,因此它们大多数都是可互换的。 cProfile的开销要低得多,但较新,并且可能并非在所有系统上都可用。 cProfile实际上是内部_lsprof模块顶部的兼容性层。 hotshot模块保留供专门使用。

Note

探查器模块旨在为给定程序提供执行配置文件,而不是出于基准测试目的(为此,提供timeit表示合理准确的结果)。这尤其适用于使用 C 代码对 Python 代码进行基准测试:分析器会为 Python 代码带来开销,但不会为 C 级函数带来开销,因此 C 代码似乎比任何 Python 代码都要快。

26.4.2. 即时用户手册

本部分适用于“不想阅读本手册”的用户。它提供了非常简短的概述,并允许用户快速对现有应用程序进行性能分析。

要分析采用单个参数的函数,可以执行以下操作:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

(如果系统上不提供profile,请使用profile而不是cProfile。)

上面的操作将运行re.compile()并打印配置文件结果,如下所示:

197 function calls (192 primitive calls) in 0.002 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.001    0.001 <string>:1(<module>)
     1    0.000    0.000    0.001    0.001 re.py:212(compile)
     1    0.000    0.000    0.001    0.001 re.py:268(_compile)
     1    0.000    0.000    0.000    0.000 sre_compile.py:172(_compile_charset)
     1    0.000    0.000    0.000    0.000 sre_compile.py:201(_optimize_charset)
     4    0.000    0.000    0.000    0.000 sre_compile.py:25(_identityfunction)
   3/1    0.000    0.000    0.000    0.000 sre_compile.py:33(_compile)

第一行表示已监视 197 个呼叫。在这些调用中,有 192 个是“原始”调用,这意味着该调用不是pass递归引起的。下一行:Ordered by: standard name,指示最右列中的文本字符串已用于对输出进行排序。列标题包括:

  • ncalls

    • 对于通话数量,
  • tottime

    • 给定Function所花费的总时间(不包括调用子Function所花费的时间)
  • percall

    • tottime除以ncalls的商
  • cumtime

    • 是此Function和所有子Function(从调用到退出)花费的累积时间。对于递归函数,该数字是准确的甚至
  • percall

    • cumtime除以原始调用的商
  • filename:lineno(function)

    • 提供每个Function的相应数据

如果第一列中有两个数字(例如3/1),则表示函数已递归。第二个值是原始调用数,前一个是调用总数。请注意,当函数不递归时,这两个值相同,并且仅打印单个图形。

您可以pass在run()函数中指定文件名来将结果保存到文件中,而不是在配置文件运行结束时打印输出:

import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')

pstats.Stats类从文件读取概要文件结果,并以各种方式格式化它们。

也可以将文件cProfile作为脚本调用,以分析另一个脚本。例如:

python -m cProfile [-o output_file] [-s sort_order] myscript.py

-o将配置文件结果写入文件,而不是标准输出

-s指定sort_stats()排序值之一来对输出进行排序。仅在未提供-o时适用。

pstats模块的Stats类具有多种方法来处理和打印保存到配置文件结果文件中的数据:

import pstats
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()

strip_dirs()方法从所有模块名称中删除了无关的路径。 sort_stats()方法根据打印的标准模块/行/名称字符串对所有条目进行排序。 print_stats()方法打印出所有统计信息。您可以try以下排序调用:

p.sort_stats('name')
p.print_stats()

第一次调用实际上将按函数名称对列表进行排序,第二次调用将打印出统计信息。以下是一些有趣的实验电话:

p.sort_stats('cumulative').print_stats(10)

这将按函数中的累积时间对配置文件进行排序,然后仅打印十个最重要的行。如果您想了解哪些算法需要时间,可以使用上面的代码。

如果您希望查看哪些函数在循环很多并且花费大量时间,则可以执行以下操作:

p.sort_stats('time').print_stats(10)

根据每个Function花费的时间进行排序,然后打印前十个Function的统计信息。

您也可以try:

p.sort_stats('file').print_stats('__init__')

这将按文件名对所有统计信息进行排序,然后仅打印类初始化方法的统计信息(因为它们中的拼写为__init__)。作为最后一个示例,您可以try:

p.sort_stats('time', 'cum').print_stats(.5, 'init')

该行使用时间的主键和累积时间的辅助键对统计信息进行排序,然后输出一些统计信息。具体来说,首先将列表缩小到其原始大小的 50%(re:.5),然后仅保留包含init的行,然后打印该子子列表。

如果您想知道调用上述函数的函数是什么,您现在可以(p仍根据最后一个条件排序):

p.print_callers(.5, 'init')

您将获得列出的每个函数的调用者列表。

如果需要更多Function,则必须阅读手册,或猜测以下Function的作用:

p.print_callees()
p.add('restats')

pstats模块作为脚本调用,是一个统计浏览器,用于读取和检查配置文件转储。它具有一个简单的面向行的界面(使用cmd实现)和交互式帮助。

26.4.3. 配置文件和 cProfile 模块参考

profilecProfile模块均提供以下Function:

  • profile. run(* command filename = None sort = -1 *)
    • 该函数采用一个可以传递给exec()函数的参数,以及一个可选的文件名。在所有情况下,此例程都将执行:
exec(command, __main__.__dict__, __main__.__dict__)

并从执行中收集分析统计信息。如果不存在文件名,则此函数自动创建一个Stats实例并打印简单的性能分析报告。如果指定了排序值,则将其传递到此Stats实例以控制如何对结果进行排序。

  • profile. runctx(* command globals locals filename = None *)
    • 此函数类似于run(),添加了用于为* command *字符串提供 globals 和 locals 字典的参数。该例程执行:
exec(command, globals, locals)

并像上面的run()函数一样收集分析统计信息。

    • class * profile. Profile(* timer = None timeunit = 0.0 subcalls = True builtins = True *)
    • 通常仅在需要对概要分析进行比cProfile.run()函数提供的控制更精确的控制时,才使用此类。

可以提供一个自定义计时器,以pass* timer 参数测量运行代码所需的时间。该函数必须返回一个代表当前时间的数字。如果数字是整数,则 timeunit *指定一个乘数,该乘数指定每个时间单位的持续时间。例如,如果计时器返回以秒为单位的时间,则时间单位为.001

直接使用Profile类可以格式化配置文件结果,而无需将配置文件数据写入文件:

import cProfile, pstats, StringIO
pr = cProfile.Profile()
pr.enable()
# ... do something ...
pr.disable()
s = StringIO.StringIO()
sortby = 'cumulative'
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print s.getvalue()
  • enable ( )

    • 开始收集分析数据。
  • disable ( )

    • 停止收集分析数据。
  • create_stats ( )

    • 停止收集概要分析数据,并将结果内部记录为当前概要文件。
  • print_stats(* sort = -1 *)

    • 根据当前配置文件创建一个Stats对象,并将结果打印到 stdout。
  • dump_stats(文件名)

    • 将当前配置文件的结果写入* filename *。
  • run(* cmd *)

    • passexec()配置 cmd。
  • runctx(* cmd globals locals *)

    • passexec()在指定的全局和本地环境中配置 cmd。
  • runcall(* func *, *args * kwargs *)

    • Profilefunc(*args, **kwargs)

26.4.4. 统计课

分析器数据的分析是使用Stats类完成的。

  • 类别 pstats. Stats(文件名或配置文件 stream = sys.stdout *)
    • 此类的构造函数从文件名(或文件名列表)或Profile实例创建“统计对象”的实例。输出将被打印到* stream *指定的流上。

由上述构造函数选择的文件必须已经由profilecProfile的相应版本创建。具体来说,此探查器的将来版本不保证文件兼容性,也不与其他探查器生成的文件或在不同 os 上运行的同一个探查器文件兼容。如果提供了多个文件,则将合并相同Function的所有统计信息,以便可以在单个报告中考虑多个过程的整体视图。如果需要将其他文件与现有Stats对象中的数据合并,则可以使用add()方法。

代替从文件读取配置文件数据,可以将cProfile.Profileprofile.Profile对象用作配置文件数据源。

Stats对象具有以下方法:

  • strip_dirs ( )

    • Stats类的此方法从文件名中删除所有前导路径信息。这对于减小打印输出的大小以适合 80 列(非常接近)非常有用。此方法修改对象,并且剥离的信息丢失。执行剥离操作后,对象被视为具有“随机”Sequences 的条目,就像对象初始化和加载之后一样。如果strip_dirs()导致两个函数名无法区分(它们在同一文件名的同一行上,并且具有相同的函数名),则这两个条目的统计信息将累积到一个条目中。
  • add(*文件名)

    • Stats类的此方法将其他配置信息累积到当前配置对象中。其参数应引用由profile.run()cProfile.run()的相应版本创建的文件名。具有相同名称(re:文件,行,名称)的函数的统计信息将自动累积到单个函数统计信息中。
  • dump_stats(文件名)

    • 将加载到Stats对象中的数据保存到名为* filename *的文件中。如果文件不存在,则创建该文件;如果文件已存在,则将其覆盖。这等效于profile.ProfilecProfile.Profile类上的同名方法。

2.3 版的新Function。

  • sort_stats(*)
    • 此方法pass根据提供的标准对Stats对象进行排序来对其进行修改。该参数通常是标识排序依据的字符串(例如'time''name')。

如果提供了多个键,则当在它们前面选择的所有键都相等时,其他键将用作次要条件。例如,sort_stats('name', 'file')将根据条目的Function名称对所有条目进行排序,并pass按文件名进行排序来解析所有联系(相同的Function名称)。

缩写可以用于任何键名,只要缩写是明确的即可。以下是当前定义的键:

Valid ArgMeaning
'calls'call count
'cumulative'cumulative time
'cumtime'cumulative time
'file'file name
'filename'file name
'module'file name
'ncalls'call count
'pcalls'原始呼叫计数
'line'line number
'name'function name
'nfl'name/file/line
'stdname'standard name
'time'internal time
'tottime'internal time

请注意,统计信息上的所有排序均以降序排列(首先放置大多数耗时的项),其中名称,文件和行号搜索按升序排列(字母 Sequences)。 'nfl''stdname'之间的细微区别是标准名称是所打印名称的一种,这意味着以奇怪的方式比较了嵌入的行号。例如,第 3、20 和 40 行(如果文件名相同)将出现在字符串 Sequences20、3 和 40 中。相反,'nfl'对行号进行数字比较。实际上,sort_stats('nfl')sort_stats('name', 'file', 'line')相同。

出于向后兼容的原因,允许使用数字参数-1012。它们分别解释为'stdname''calls''time''cumulative'。如果使用此旧样式格式(数字),将仅使用一个排序键(数字键),而其他参数将被忽略。

  • reverse_order ( )

    • Stats类的此方法可反转对象中基本列表的 Sequences。请注意,默认情况下,根据选择的排序键正确选择了升序还是降序。
  • print_stats((*限制)

打印 Sequences 基于对对象执行的最后sort_stats()操作(需注意add()strip_dirs()中的警告)。

提供的参数(如果有)可用于将列表限制为有效条目。最初,该列表被视为完整的概要分析Function集。每个限制可以是一个整数(选择行数),或者是 0.0 到 1.0 之间的一个十进制小数(包括行数的百分比),或者是一个正则表达式(以匹配打印的标准名称)。提供了限制,然后按 Sequences 应用它们,例如:

print_stats(.1, 'foo:')

首先将打印限制在列表的前 10%,然后仅打印文件名.*foo:中的Function。相反,该命令:

print_stats('foo:', .1)

会将列表限制为具有文件名.*foo:的所有函数,然后 continue 仅打印其中的前 10%。

  • print_callers((*限制)

    • Stats类的此方法将打印所有概要文件数据库中调用每个函数的函数的列表。Sequences 与print_stats()提供的 Sequences 相同,并且限制参数的定义也相同。每个呼叫者都在自己的线路上报告。格式略有不同,具体取决于生成统计信息的探查器:
  • 使用profile时,在每个呼叫者之后的括号中显示一个数字,以显示进行此特定呼叫的次数。为方便起见,第二个非括号内的数字重复了右侧Function所花费的累积时间。

  • 使用cProfile时,每个调用方前面都有三个数字:进行此特定调用的次数,以及在此特定调用方调用当前函数时在该函数中花费的总时间和累积时间。

  • print_callees((*限制)

    • Stats类的此方法将打印所指示函数调用的所有函数的列表。除了这种调用方向的反转(re:被调用 vs 被调用)之外,参数和 Sequences 与print_callers()方法相同。

26.4.5. 什么是确定性分析?

“确定性分析”旨在反映以下事实:监视所有* function call function return exception *事件,并对这些事件之间的间隔进行精确计时(在这段时间内执行用户代码) )。相反,统计分析(此模块未完成)对有效指令指针进行随机采样,并推断出花费的时间。传统上,后一种技术涉及较少的开销(因为不需要检测代码),但是仅提供时间花费在哪里的相对指示。

在 Python 中,由于在执行期间有活动的解释器,因此不需要执行检测代码即可进行确定性分析。 Python 自动为每个事件提供一个* hook *(可选的回调)。另外,Python 的解释性性质往往会增加执行开销,以至于确定性分析往往只会增加典型应用程序中的少量处理开销。结果是确定性分析并没有那么昂贵,但是提供了有关 Python 程序执行的大量运行时统计信息。

调用计数统计信息可用于识别代码中的错误(令人惊讶的计数),并标识可能的内联扩展点(较高的调用计数)。内部时间统计信息可用于识别应仔细优化的“热循环”。在选择算法时,应使用累积时间统计信息来识别高级错误。请注意,此探查器中对累积时间的异常处理允许将算法的递归实现的统计信息直接与迭代实现进行比较。

26.4.6. Limitations

一个限制与定时信息的准确性有关。确定性分析器存在一个涉及准确性的基本问题。最明显的限制是底层的“时钟”仅以约.001 秒的速率滴答(通常)。因此,没有任何测量比基础时钟更准确。如果进行了足够的测量,则“误差”将趋于平均。不幸的是,消除此第一个错误会引起第二个错误源。

第二个问题是,从调度事件开始到探查器调用以获取时间实际上“获取”时钟状态需要“一段时间”。类似地,从获取时钟值(然后松散)开始,退出探查器事件处理程序会有一定的滞后,直到再次执行用户的代码为止。结果,被多次调用或调用许多函数的函数通常会累积此错误。以这种方式累积的误差通常小于时钟的精度(小于一个时钟滴答声),但是它可以累积并变得非常重要。

对于profile而言,此问题比开销较低的cProfile更为重要。因此,profile提供了一种针对给定平台进行自我校准的方法,以便可以(平均)概率地消除此错误。在对探查器进行校准之后,它会更加精确(至少在平方意义上),但有时会产生负数(当呼叫计数异常低时,概率之神对您不利:-)。 )不要在配置文件中被负数警告。如果您已经校准了探查器,它们应该出现,并且结果实际上比没有校准要好。

26.4.7. Calibration

profile模块的事件探查器从每个事件处理时间中减去一个常数,以补偿调用时间函数并存储结果的开销。默认情况下,该常数为 0.可以使用以下过程为给定平台获得更好的常数(请参见Limitations)。

import profile
pr = profile.Profile()
for i in range(5):
    print pr.calibrate(10000)

该方法直接在分析器下一次又一次地执行参数给出的 Python 调用次数,从而测量两者的时间。然后,它计算每个事件探查器事件的隐藏开销,并将其作为浮点数返回。例如,在运行 Mac OS X 且使用 Python 的 time.clock()作为计时器的 1.8Ghz Intel Core i5 上,神奇的数字约为 4.04e-6.

此练习的目的是获得一个相当一致的结果。如果您的计算机非常快,或者计时器Function的分辨率很差,则可能必须传递 100000 甚至 1000000 才能获得一致的结果。

当您获得一致的答案时,可以使用三种方法:[1]

import profile

# 1. Apply computed bias to all Profile instances created hereafter.
profile.Profile.bias = your_computed_bias

# 2. Apply computed bias to a specific Profile instance.
pr = profile.Profile()
pr.bias = your_computed_bias

# 3. Specify computed bias in instance constructor.
pr = profile.Profile(bias=your_computed_bias)

如果可以选择,最好选择一个较小的常数,然后结果在配置文件统计信息中“较少出现”为负数。

26.4.8. 使用自定义计时器

如果要更改确定当前时间的方式(例如,强制使用壁钟时间或经过的处理时间),请将所需的计时函数传递给Profile类构造函数:

pr = profile.Profile(your_time_func)

然后,生成的探查器将调用your_time_func。根据您使用的是profile.Profile还是cProfile.Profileyour_time_func的返回值将被不同地解释:

  • profile.Profile

    • your_time_func应该返回一个数字,或者返回其总和为当前时间的数字列表(例如os.times()返回的数字)。如果该函数返回单个时间编号,或者返回的编号列表的长度为 2,则您将获得调度程序特别快的版本。

警告您应该为所选的计时器函数校准事件探查器类(请参见Calibration)。对于大多数计算机而言,返回一个单独的整数值的计时器将提供最佳的结果,因为它们在分析过程中的开销较低。 (os.times()很差*,因为它返回一个浮点值的 Tuples)。如果您想以最简洁的方式替换更好的计时器,请派生一个类,并用硬接线方式替代最能处理您的计时器调用的替换调度方法以及适当的校准常数。

  • cProfile.Profile

    • your_time_func应该返回一个数字。如果返回整数,则还可以使用第二个参数调用类构造函数,该参数指定一个时间单位的实际持续时间。例如,如果your_integer_time_func返回以千秒为单位的时间,则可以按以下方式构造Profile实例:
pr = cProfile.Profile(your_integer_time_func, 0.001)

由于无法校准cProfile.Profile类,因此应谨慎使用自定义计时器Function,并且应尽可能快。为了使用自定义计时器获得最佳结果,可能有必要在内部_lsprof模块的 C 源代码中对其进行硬编码。

Footnotes

  • [1]
    • 在 Python 2.2 之前,有必要编辑探查器源代码以将偏差作为 Literals 数字嵌入。您仍然可以,但是不再描述该方法,因为不再需要。