"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 实现会引入显著开销。

对于大多数性能分析任务，请使用：

* "profiling.sampling" ，用于零开销的生产环境调试

* "profiling.tracing" ，用于开发和测试


迁移
====

从 "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" 的现有代码无需修改即可继续工作。


"profile" 和 "profiling.tracing" 模块参考
=========================================

"profile" 和 "profiling.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" 模块不支持；请参阅 Context Manager Types
   ）:

      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.Profile" ，"your_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"
     性能分析数据的统计分析与格式化。
