关于 sys.monitoring
的说明
注意
本文档是在 Python 3.12 问世时编写的。未来版本的 Python 可能会有不同的行为。然而,我们希望其中大部分概念仍将保持相关性。
Python 3.12 在 sys.monitoring
下引入了一个新的监控系统。该系统允许用户监控一系列事件,这些事件可能对性能分析或调试等目的有用。事件监控是“按工具”设置的,因此可以同时运行多个工具。对于每个工具,事件可以按线程全局监控,或按代码对象局部监控(或两者的混合)。对于每个工具-事件组合,可以注册一个回调函数,该函数将在事件发生时被调用。回调函数只是常规函数,可以执行 Python 支持的大部分操作,它们还能够返回一个特殊值,以告知监控系统禁用当前代码位置的未来事件触发。
这对 Numba 意味着什么?
当解释器“遇到”监控事件(它实际上是发出这些事件)时,它会触发与该事件相关联的所有工具已注册监控该事件的回调函数。对于 Numba 而言,存在一些问题……
Numba 的设计使得函数执行过程中不涉及 Python 解释器,函数被编译,其执行路径仅存在于机器码中。要从解释器跳转到机器码,需要调用 Numba 调度器,这是(在 nopython
模式下)栈中 Python 解释器随时可用的最后一个地方。调度器也在某种程度上是函数执行的一部分,没有调度器,从用户空间很难直接调用机器码。因此,Numba 可以支持的监控类型和事件类型受到一定限制,因为执行过程中解释器的参与非常有限!
接下来我们逐一看看监控类型。通过在代码对象上设置监控来请求局部监控。实际上,这会指示解释器在运行时通过将某些操作码替换为“已插桩”操作码来增强字节码。这些已插桩操作码通过解释器循环中的特殊路径,从而在与特定指令和特定偏移量关联时发出一个“事件”。例如,一个 RETURN
操作码可能会被 INSTRUMENTED_RETURN
替换,当已插桩指令被解释时,会发出一个 PY_RETURN
事件。该事件及其发生的偏移量会被转发到监控系统。不幸的是,这给 Numba 带来了问题,因为执行过程中不涉及解释器,因此不会发出事件。通过在调度时分析代码对象,似乎有可能处理少数几种事件,例如 PY_START
和 PY_RETURN
。然而,用户有可能在执行过程中取消对代码对象的插桩和/或动态禁用特定代码位置的监控,因此,模拟这种语义将极具挑战性,并且可能需要与解释器持续交互。因此,Numba 不支持局部事件监控,即使设置了,编译后的函数仍将正确执行,只是对 sys.monitoring
没有影响。
考虑到按线程的全局监控,这表现为用户为给定线程在解释器上设置一些全局状态。该状态可以通过 sys.monitoring
Python API 访问,也可以通过 CPython 内部机制访问。这种监控方式更适合与 Numba 配合使用,因为不涉及代码对象,并且执行过程中的状态变异只能通过对象模式调用发生。
Numba 实际如何操作?
由于没有用于发出事件的 Python 或 C API(这个概念与解释器本身紧密相关),Numba 必须在调度序列中的适当位置查找工具-事件组合,然后手动调用相关联的回调函数(本质上是执行解释器发出事件时所做的工作)。对于 Numba 调度器而言,只有少数几个事件是相关的,并且只支持以下四种:
sys.monitoring.events.PY_START
(Python 函数开始执行)。sys.monitoring.events.PY_RETURN
(Python 函数返回)。sys.monitoring.events.RAISE
(Python 函数引发异常)。sys.monitoring.events.PY_UNWIND
(Python 函数在异常展开期间退出)。
这些事件实际上不存在于机器码中,但如果解释器解释了等效的字节码,它们就会存在。因此,调度器在控制权转移到机器码之前,会检查 PY_START
的监控,并调用任何相关联的回调函数。对于 PY_RETURN
也是如此,它在控制权从机器码传回调度器之后进行。这种行为实质上模拟了解释器执行字节码,并允许 cProfile
等工具能够“看到” Numba 编译的函数作为标准解释执行的一部分。如果机器码中引发了异常,相关的错误状态会在控制权传回调度器之后立即处理,此时会检查 RAISE
和 PY_UNWIND
事件的监控,并调用已注册的回调函数。
关于偏移量的说明。回调函数通常接受一个“偏移量”参数,该参数表示触发回调的事件在字节码中遇到的偏移量。对于 PY_START
,这似乎与 RESUME
字节码的偏移量相关联。对于 PY_RETURN
,这与其中一个 RETURN
字节码的偏移量相关联,通常这只能在运行时得知,因为可能存在多个返回路径。因此,Numba 选择将所有偏移量设置为零。最终可能可以通过一些分析并将适当的运行时信息从机器码传输到调度器,然而,目前为此付出的努力远远超过了所获得的收益。