关于 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 选择将所有偏移量设置为零。最终可能可以通过一些分析将适当的运行时信息从机器代码传递给调度器,然而,目前这样做所付出的努力远远超过了收益。