profile --- 纯 Python 性能分析器

源代码: Lib/profile.py


从 3.15 版起已弃用,将在 3.17 版中移除.

The profile module is deprecated and will be removed in Python 3.17. Use profiling.tracing instead.

profile 模块提供了确定性性能分析器的纯 Python 实现。虽然它有助于理解性能分析器内部机制,或通过子类化扩展性能分析器行为,但与基于 C 的 profiling.tracing 模块相比,它的纯 Python 实现会引入显著开销。

对于大多数性能分析任务,请使用:

迁移

profile 迁移到 profiling.tracing 很直接。它们的 API 是兼容的:

# 旧方式(已弃用)\nimport profile\nprofile.run('my_function()')\n\n# 新方式(推荐)\nimport profiling.tracing\nprofiling.tracing.run('my_function()')

对于大多数代码,将 import profile 替换为 import profiling.tracing``(并在所有地方使用 ``profiling.tracing 而不是 profile)即可提供一条直接的迁移路径。

备注

cProfile 模块仍作为 profiling.tracing 的向下兼容别名可用。使用 import cProfile 的现有代码无需修改即可继续工作。

profileprofiling.tracing 模块参考

profileprofiling.tracing 模块都提供以下函数:

profile.run(command, filename=None, sort=-1)

此函数接受一个可被传递给 exec() 函数的单独参数,以及一个可选的文件名。在所有情况下这个例程都会执行:

exec(command, __main__.__dict__, __main__.__dict__)

并收集执行过程中的性能分析统计数据。如果未提供文件名,则此函数会自动创建一个 Stats 实例并打印一个简单的性能分析报告。如果指定了 sort 值,则它会被传递给这个 Stats 实例以控制结果的排序方式。

profile.runctx(command, globals, locals, filename=None, sort=-1)

此函数类似于 run() ,带有为 command 字符串提供 globals 和 locals 映射对象的附加参数。这个例程会执行:

exec(command, globals, locals)

并像在上述的 run() 函数中一样收集性能分析数据。

class profile.Profile(timer=None, timeunit=0.0, subcalls=True, builtins=True)

通常只有在需要比 profiling.tracing.run() 函数所提供的控制更精细地控制性能分析时,才会使用此类。

可以通过 timer 参数提供一个自定义计时器来测量代码运行花费了多长时间。它必须是一个返回代表当前时间的单个数字的函数。如果该数字为整数,则 timeunit 指定一个表示每个时间单位持续时间的乘数。例如,如果定时器返回以千秒为计量单位的时间值,则时间单位将为 .001

直接使用 Profile 类将允许格式化性能分析结果而无需将性能分析数据写入到文件:

import profiling.tracing\nimport pstats\nimport io\nfrom pstats import SortKey\n\npr = profiling.tracing.Profile()\npr.enable()\n# ... 做一些操作 ...\npr.disable()\ns = io.StringIO()\nsortby = SortKey.CUMULATIVE\nps = pstats.Stats(pr, stream=s).sort_stats(sortby)\nps.print_stats()\nprint(s.getvalue())

Profile 类也可作为上下文管理器使用 (仅在 profiling.tracing 中支持,已弃用的 profile 模块不支持;请参阅 上下文管理器类型 ):

import profiling.tracing\n\nwith profiling.tracing.Profile() as pr:\n    # ... 做一些操作 ...\n\n    pr.print_stats()

在 3.8 版本发生变更: 添加了上下文管理器支持。

enable()

开始收集性能分析数据。仅在 profiling.tracing 中可用。

disable()

停止收集性能分析数据。仅在 profiling.tracing 中可用。

create_stats()

停止收集分析数据,并在内部将结果记录为当前 profile。

print_stats(sort=-1)

根据当前性能分析数据创建一个 Stats 对象并将结果打印到 stdout。

sort 形参指定所显示统计信息的排序顺序。它接受单个键或由键组成的元组,以启用多级排序,与 pstats.Stats.sort_stats() 中一样。

Added in version 3.13: 现在 print_stats() 可接受一个由键组成的元组。

dump_stats(filename)

将当前 profile 的结果写入 filename

run(cmd)

通过 exec() 对该命令进行性能分析。

runctx(cmd, globals, locals)

通过 exec() 并附带指定的全局和局部环境对该命令进行性能分析。

runcall(func, /, *args, **kwargs)

func(*args, **kwargs) 进行性能分析

请注意性能分析只有在被调用的命令/函数确实能返回时才可用。如果解释器被终结(例如在被调用的命令/函数执行期间通过 sys.exit() 调用)则将不会打印性能分析结果。

profiling.tracing 的差异

profile 模块与 profiling.tracing 在多个方面存在差异:

开销更高。 纯 Python 实现明显慢于 C 实现,因此不适合分析长时间运行的程序或对性能敏感的代码。

校准支持。 profile 模块支持校准,以补偿性能分析开销。profiling.tracing 不需要这样做,因为 C 实现的开销可以忽略不计。

自定义计时器。 两个模块都支持自定义计时器,但 profile 接受返回元组的计时器函数 (如 os.times() ),而 profiling.tracing 要求函数返回单个数字。

子类化。 纯 Python 实现更容易通过子类化来扩展,以实现自定义性能分析行为。

什么是确定性性能分析?

确定性性能分析 意味着所有 函数调用函数返回异常 事件都会受到监视,并会对这些事件之间的间隔(用户代码正在执行的时间)进行精确计时。相比之下,统计性性能分析 (由 profiling.sampling 模块提供)会周期性地对有效指令指针进行采样,并推断时间花费在哪里。后一种技术传统上开销更小(因为代码不需要被插桩),但只能相对地指出时间花费在哪里。

在 Python 中,由于在执行过程中总有一个活动的解释器,因此执行确定性评测不需要插入指令的代码。Python 自动为每个事件提供一个 钩子 (可选回调)。此外,Python 的解释特性往往会给执行增加太多开销,以至于在典型的应用程序中,确定性分析往往只会增加很小的处理开销。结果是,确定性分析并没有那么代价高昂,但是它提供了有关 Python 程序执行的大量运行时统计信息。

调用计数统计信息可用于识别代码中的错误(意外计数),并识别可能的内联扩展点(高频调用)。内部时间统计可用于识别应仔细优化的 "热循环" 。累积时间统计可用于识别算法选择上的高级别错误。请注意,该分析器中对累积时间的异常处理,允许直接比较算法的递归实现与迭代实现的统计信息。

局限性

一个限制是关于时间信息的准确性。确定性性能分析存在一个涉及精度的基本问题。最明显的限制是,底层的 “时钟” 周期大约为 0.001 秒(通常)。因此,没有什么测量会比底层时钟更精确。如果进行了足够的测量,那么 “误差” 将趋于平均。不幸的是,消除第一个误差会引入第二个误差来源。

第二个问题是,从调度事件到分析器调用获取时间函数实际 获取 时钟状态,这需要 "一段时间" 。类似地,从获取时钟值(然后保存)开始,直到再次执行用户代码为止,退出分析器事件句柄时也存在一定的延迟。因此,多次调用单个函数或调用多个函数通常会累积此错误。尽管这种方式的误差通常小于时钟的精度(小于一个时钟周期),但它 可以 累积并变得非常可观。

与开销较低的 profiling.tracing 相比,这个问题对于已弃用的 profile 模块更为重要。因此,profile 提供了一种针对给定平台校准自身的方法,使这种误差可以按概率方式(平均而言)被消除。性能分析器经过校准后会更加准确(按最小二乘意义),但有时会产生负数(当调用次数异常低,并且概率之神与你作对时 :-))。不要因为性能分析结果中出现负数而惊慌。它们应该 只会 在你校准过性能分析器时出现,而且结果实际上会比未校准时更好。

校准

profile 模块的性能分析器会从每次事件处理时间中减去一个常量,以补偿调用计时函数和保存结果的开销。默认情况下,该常量为 0。可以使用以下过程为给定平台获得一个更好的常量 (参见 局限性 )。:

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

此方法将执行由参数所给定次数的 Python 调用,在性能分析器之下直接和再次地执行,并对两次执行计时。 它将随后计算每个性能分析器事件的隐藏开销,并将其以浮点数的形式返回。例如,在一台运行 macOS 的 1.8Ghz Intel Core i5 上,使用 Python 的 time.process_time() 作为计时器,魔数大约为 4.04e-6。

此操作的目标是获得一个相当稳定的结果。如果你的计算机 非常 快速,或者你的计时器函数的分辨率很差,你可能必须传入 100000,甚至 1000000,才能得到稳定的结果。

当你有一个一致的答案时,有三种方法可以使用:

import profile

# 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)

如果你可以选择,那么选择更小的常量会更好,这样你的结果将“更不容易”在性能分析统计中显示负值。

使用自定义计时器

如果你想要改变当前时间的确定方式(例如,强制使用时钟时间或进程持续时间),请向 Profile 类构造器传入你想要的计时函数:

pr = profile.Profile(your_time_func)

得到的性能分析器随后会调用 your_time_func。根据你使用的是 profile.Profile 还是 profiling.tracing.Profileyour_time_func 的返回值会以不同方式解释:

profile.Profile

your_time_func 应当返回一个数字,或一个总和为当前时间的数字列表 (如同 os.times() 所返回的内容)。 如果该函数返回一个数字,或所返回的数字列表长度为 2,则你将得到一个特别快速的调度例程版本。

请注意你应当为你选择的计时器函数校准性能分析器类 (参见 校准)。 对于大多数机器来说,一个返回长整数值的计时器在性能分析期间将提供在低开销方面的最佳结果。 (os.times()相当 糟糕的,因为它返回一个浮点数值的元组)。 如果你想以最干净的方式替换一个更好的计时器,请派生一个类并硬连线一个能最佳地处理计时器调用的替换调度方法,并使用适当的校准常量。

profiling.tracing.Profile

your_time_func 应当返回一个数字。如果它返回整数,你还可以通过第二个参数指定一个单位时间的实际持续长度来唤起类构造器。 举例来说,如果 your_integer_time_func 返回以千秒为单位的时间,则你应当以如下方式构造 Profile 实例:

pr = profiling.tracing.Profile(your_integer_time_func, 0.001)

由于 profiling.tracing.Profile 类无法校准,因此使用自定义计时器函数时应当谨慎,并应使其尽可能快。为了通过自定义计时器获得最佳结果,可能需要在内部 _lsprof 模块的 C 源码中将其硬编码。

Python 3.3 在 time 中添加了几个可被用来精确测量进程或时钟时间的新函数。例如,参见 time.perf_counter().

参见

profiling

Python 性能分析工具概述。

profiling.tracing

此模块的推荐替代项。

pstats

性能分析数据的统计分析与格式化。