Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 669 – CPython 低影响监控

作者:
Mark Shannon <mark at hotpy.org>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
创建日期:
2021年8月18日
Python 版本:
3.12
发布历史:
2021年12月7日, 2022年1月10日
决议:
Discourse 消息

目录

重要

本PEP是一个历史文档。最新的、权威的文档现在可以在 sys.monitoring 找到。

×

有关如何提出更改,请参阅 PEP 1

摘要

在CPython中使用分析器或调试器可能会严重影响性能。性能下降一个数量级是很常见的。

本PEP提出了一种用于监控CPython上运行的Python程序的API,该API将以低成本实现监控。

尽管本PEP未指定实现,但预计它将使用 PEP 659 的快速化步骤来实现。

将添加一个 sys.monitoring 命名空间,其中将包含相关函数和常量。

动机

开发人员不应为使用调试器、分析器和其他类似工具付出不合理的代价。

C++ 和 Java 开发人员期望能够在调试器下全速(或接近全速)运行程序。Python 开发人员也应该有这种期望。

基本原理

PEP 659 提供的快速化机制提供了一种动态修改正在执行的 Python 字节码的方法。这些修改的成本除了被修改的代码部分之外很小,并且对于被修改的部分来说成本相对较低。我们可以利用这一点来提供一种在 3.10 或更早版本中不可能实现的高效监控机制。

通过使用快速化,我们期望在 3.12 上在调试器下运行的代码应该优于在 3.11 上不带调试器运行的代码。分析仍然会减慢执行速度,但比 3.11 慢得多。

规范

Python 程序的监控是通过为事件注册回调函数和激活一组事件来完成的。

激活事件和注册回调函数是相互独立的。

注册回调和激活事件都是按工具进行的。可以有多个工具响应不同的事件集。

请注意,与 sys.settrace() 不同,事件和回调是每个解释器而不是每个线程的。

事件

当代码对象执行时,会发生各种工具可能感兴趣的事件。通过激活事件和注册回调函数,工具可以以任何适合它们的方式响应这些事件。事件可以全局设置,也可以为单个代码对象设置。

对于 3.12,CPython 将支持以下事件

  • PY_START:Python 函数的开始(在调用后立即发生,被调用者的帧将在堆栈上)
  • PY_RESUME:Python 函数的恢复(对于生成器和协程函数),除了 throw() 调用。
  • PY_THROW:Python 函数通过 throw() 调用恢复。
  • PY_RETURN:Python 函数的返回(在返回前立即发生,被调用者的帧将在堆栈上)。
  • PY_YIELD:Python 函数的 yield(在 yield 前立即发生,被调用者的帧将在堆栈上)。
  • PY_UNWIND:在异常展开期间从 Python 函数退出。
  • CALL:Python 代码中的调用(事件在调用之前发生)。
  • C_RETURN:从任何可调用对象返回,除了 Python 函数(事件在返回后发生)。
  • C_RAISE:从任何可调用对象引发异常,除了 Python 函数(事件在退出后发生)。
  • RAISE:引发异常,除了那些导致 STOP_ITERATION 事件的异常。
  • EXCEPTION_HANDLED:异常已处理。
  • LINE:即将执行的指令的行号与前一条指令的行号不同。
  • INSTRUCTION – VM 指令即将执行。
  • JUMP – 控制流图中的无条件跳转。
  • BRANCH – 条件分支被采用(或不被采用)。
  • STOP_ITERATION – 引发人工的 StopIteration;请参阅 STOP_ITERATION 事件

将来可能会添加更多事件。

所有事件都将是 sys.monitoringevents 命名空间的属性。所有事件都将由一个2的幂的整数表示,因此它们可以通过 | 运算符进行组合。

事件分为三组

本地事件

本地事件与程序的正常执行相关联,并发生在明确定义的位置。所有本地事件都可以禁用。本地事件有

  • PY_START
  • PY_RESUME
  • PY_RETURN
  • PY_YIELD
  • CALL
  • LINE
  • INSTRUCTION
  • JUMP
  • BRANCH
  • STOP_ITERATION

辅助事件

辅助事件可以像其他事件一样被监控,但由另一个事件控制

  • C_RAISE
  • C_RETURN

C_RETURNC_RAISE 事件由 CALL 事件控制。C_RETURNC_RAISE 事件只有在相应的 CALL 事件被监控时才会出现。

其他事件

其他事件不一定与程序中的特定位置绑定,并且不能单独禁用。

可以监控的其他事件有

  • PY_THROW
  • PY_UNWIND
  • RAISE
  • EXCEPTION_HANDLED

STOP_ITERATION 事件

PEP 380 指定在从生成器或协程返回值时会引发 StopIteration 异常。然而,这是一种非常低效的返回值方式,因此一些 Python 实现,特别是 CPython 3.12+,除非其他代码可见,否则不会引发异常。

为了允许工具监控实际异常而不会减慢生成器和协程的速度,提供了 STOP_ITERATION 事件。STOP_ITERATION 可以局部禁用,而 RAISE 则不能。

工具标识符

虚拟机最多可以同时支持6个工具。在注册或激活事件之前,工具应选择一个标识符。标识符是0到5范围内的整数。

sys.monitoring.use_tool_id(id, name:str) -> None
sys.monitoring.free_tool_id(id) -> None
sys.monitoring.get_tool(id) ->  str | None

如果 id 正在使用中,sys.monitoring.use_tool_id 会引发 ValueError。如果 id 正在使用中,sys.monitoring.get_tool 返回工具的名称,否则返回 None

所有ID在VM中对于事件的处理方式相同,但预定义了以下ID以方便工具协作

sys.monitoring.DEBUGGER_ID = 0
sys.monitoring.COVERAGE_ID = 1
sys.monitoring.PROFILER_ID = 2
sys.monitoring.OPTIMIZER_ID = 5

没有义务设置ID,也没有任何东西阻止工具使用即使已被占用的ID。但是,鼓励工具使用唯一的ID并尊重其他工具。

例如,如果调试器已附加且 DEBUGGER_ID 正在使用中,它应该报告错误,而不是不顾一切地继续执行。

OPTIMIZER_ID 为 Cinder 或 PyTorch 等希望优化 Python 代码但需要以依赖于更广泛上下文的方式决定优化内容的工具提供。

全局设置事件

事件可以通过修改被监控的事件集进行全局控制

  • sys.monitoring.get_events(tool_id:int)->int 返回表示所有活动事件的 int
  • sys.monitoring.set_events(tool_id:int, event_set: int) 激活 event_set 中设置的所有事件。如果 tool_id 未在使用中,则引发 ValueError

默认情况下没有活动事件。

每个代码对象事件

事件也可以按代码对象进行控制

  • sys.monitoring.get_local_events(tool_id:int, code: CodeType)->int 返回 code 的所有本地事件
  • sys.monitoring.set_local_events(tool_id:int, code: CodeType, event_set: int) 激活 event_set 中设置的 code 的所有本地事件。如果 tool_id 未在使用中,则引发 ValueError

本地事件会添加到全局事件中,但不会屏蔽它们。换句话说,所有全局事件都会为代码对象触发,无论本地事件如何。

注册回调函数

要为事件注册可调用对象,请调用

sys.monitoring.register_callback(tool_id:int, event: int, func: Callable | None) -> Callable | None

如果已为给定的 tool_idevent 注册了另一个回调,则该回调将被注销并返回。否则,register_callback 返回 None

可以通过调用 sys.monitoring.register_callback(tool_id, event, None) 来注销函数。

回调函数可以随时注册和注销。

注册或注销回调函数将生成一个 sys.audit 事件。

回调函数参数

当活动事件发生时,会调用注册的回调函数。不同的事件将向回调函数提供不同的参数,如下所示

  • PY_STARTPY_RESUME
    func(code: CodeType, instruction_offset: int) -> DISABLE | Any
    
  • PY_RETURNPY_YIELD
    func(code: CodeType, instruction_offset: int, retval: object) -> DISABLE | Any
  • CALL, C_RAISEC_RETURN
    func(code: CodeType, instruction_offset: int, callable: object, arg0: object | MISSING) -> DISABLE | Any

    如果没有参数,arg0 将设置为 MISSING

  • RAISEEXCEPTION_HANDLED
    func(code: CodeType, instruction_offset: int, exception: BaseException) -> DISABLE | Any
  • LINE:
    func(code: CodeType, line_number: int) -> DISABLE | Any
  • BRANCH:
    func(code: CodeType, instruction_offset: int, destination_offset: int) -> DISABLE | Any

    请注意,destination_offset 是代码下一步将执行的位置。对于未采取的分支,这将是分支后面指令的偏移量。

  • INSTRUCTION:
    func(code: CodeType, instruction_offset: int) -> DISABLE | Any

如果回调函数返回 DISABLE,则该函数将不再为该 (code, instruction_offset) 调用,直到调用 sys.monitoring.restart_events()。此功能适用于覆盖率和其他只对看到一次事件感兴趣的工具。

请注意,sys.monitoring.restart_events() 不是特定于某个工具的,因此工具必须准备好接收它们选择 DISABLE 的事件。

回调函数中的事件

对于注册了该回调的工具,回调函数及其被调用函数中的事件将暂停。

这意味着其他工具将在其他工具的回调函数中看到事件。这对于调试分析工具可能很有用,但会产生误导性分析结果,因为调试器工具将显示在分析中。

事件顺序

如果一条指令触发多个事件,它们将按以下顺序发生

  • LINE
  • INSTRUCTION
  • 所有其他事件(每条指令只能发生其中一个事件)

每个事件都按照 ID 的升序传递给工具。

“call”事件组

大多数事件是独立的;设置或禁用一个事件对其他事件没有影响。然而,CALLC_RAISEC_RETURN 事件构成一个组。如果这些事件中的任何一个被设置或禁用,那么该组中的所有事件都将被设置或禁用。禁用 CALL 事件不会禁用匹配的 C_RAISEC_RETURN,但会禁用所有后续事件。

sys.monitoring 命名空间的属性

  • def use_tool_id(id)->None
  • def free_tool_id(id)->None
  • def get_events(tool_id: int)->int
  • def set_events(tool_id: int, event_set: int)->None
  • def get_local_events(tool_id: int, code: CodeType)->int
  • def set_local_events(tool_id: int, code: CodeType, event_set: int)->None
  • def register_callback(tool_id: int, event: int, func: Callable)->Optional[Callable]
  • def restart_events()->None
  • DISABLE: object
  • MISSING: object

访问“仅调试”功能

标准库的一些功能对普通代码不可访问,但对调试器可访问。例如,设置局部变量或行号。

这些功能将对回调函数可用。

向后兼容性

本PEP在很大程度上是向后兼容的。

PEP 523 存在一些兼容性问题,因为 PEP 523 插件的行为不受 VM 控制。由 PEP 523 插件确保它们遵守本 PEP 的语义。不改变 VM 状态并将执行委托给 _PyEval_EvalFrameDefault() 的简单插件应该继续工作。

sys.settrace()sys.setprofile() 将分别作为工具 6 和 7,因此可以与本 PEP 一起使用。

这意味着 sys.settrace()sys.setprofile() 可能无法与所有 PEP 523 插件正常工作。尽管如上所述的简单 PEP 523 插件应该没有问题。

性能

如果没有活动事件,本 PEP 应该对性能产生小的积极影响。实验表明,不直接支持 sys.settrace() 可以带来 1% 到 2% 的加速。

sys.settrace() 的性能将大致相同。sys.setprofile() 的性能应该更好。然而,依赖于 sys.settrace()sys.setprofile() 的工具可以通过使用本 PEP 提供的 API 变得更快。

如果只有少量事件处于活动状态,例如用于调试器,那么回调的开销将比 sys.settrace() 小几个数量级,并且比使用 PEP 523 便宜得多。

通过在所有回调中返回 DISABLE,可以以非常低的成本实现代码覆盖率工具。

对于高度插装的代码,例如使用 LINE,性能应该优于 sys.settrace,但不会好很多,因为性能将主要由回调中花费的时间决定。

对于优化虚拟机,例如未来版本的 CPython(以及 PyPy 如果他们选择支持此 API),在长时间运行的程序中更改活动事件集可能会非常昂贵,可能需要数百毫秒,因为它会触发去优化。一旦发生这种去优化,性能应该会恢复,因为虚拟机可以重新优化插装代码。

通常这些操作可以认为是快速的

  • def get_events(tool_id: int)->int
  • def get_local_events(tool_id: int, code: CodeType)->int
  • def register_callback(tool_id: int, event: int, func: Callable)->Optional[Callable]
  • def get_tool(tool_id) -> str | None

这些操作较慢,但并非特别慢

  • def set_local_events(tool_id: int, code: CodeType, event_set: int)->None

这些操作应被视为缓慢

  • def use_tool_id(id, name:str)->None
  • def free_tool_id(id)->None
  • def set_events(tool_id: int, event_set: int)->None
  • def restart_events()->None

慢操作的速度取决于它们发生的时间。如果它们在程序早期,在模块加载之前完成,它们应该相当便宜。

内存消耗

不使用时,本PEP对内存消耗的影响可以忽略不计。

内存的使用方式在很大程度上是实现细节。但是,我们预计对于3.12,每个代码对象的额外内存消耗将 大致 如下

事件
工具 其他 LINE INSTRUCTION
一个 ≈40% ≈80%
两个或更多 ≈40% ≈120% ≈200%

安全隐患

允许修改正在运行的代码具有一些安全隐患,但不会比生成和调用新代码的能力更多。

上述所有新函数都将触发审计钩子。

实施

本节概述了CPython 3.12的拟议实现。CPython后续版本和其他Python实现的实际实现可能存在很大差异。

本PEP的拟议实现将基于CPython 3.11的快速化步骤,如 PEP 659 中所述。插装的工作方式与快速化大致相同,字节码会根据需要替换为已插装的字节码。

例如,如果 CALL 事件被开启,那么所有调用指令都将被替换为 INSTRUMENTED_CALL 指令。

请注意,这将干扰专门化,除了调用注册的可调用对象的开销之外,还会导致一些性能下降。

当活动事件集发生变化时,VM 将立即更新所有线程调用堆栈上存在的代码对象。它还将设置陷阱,以确保在调用时正确地对所有代码对象进行插装。因此,更改活动事件集应尽可能少地进行,因为这可能是一项非常昂贵的操作。

其他事件,例如 RAISE,可以廉价地开启或关闭,因为它们不依赖于代码插装,而是依赖于底层事件发生时的运行时检查。

需要插装的事件集是实现细节,但对于当前设计,以下事件将需要插装

  • PY_START
  • PY_RESUME
  • PY_RETURN
  • PY_YIELD
  • CALL
  • LINE
  • INSTRUCTION
  • JUMP
  • BRANCH

每个经过检测的字节码将需要额外的8位信息来注明该检测适用于哪个工具。LINEINSTRUCTION 事件需要额外的信息,因为它们需要存储原始指令,甚至如果它们与其他检测重叠,则需要存储经过检测的指令。

实现工具

本 PEP 的理念是,第三方监控工具应该能够实现高性能,而不是让它们容易实现高性能。

将事件转换为对用户有意义的数据是工具的责任。

所有事件都有成本,工具应尝试使用触发频率最低但仍能提供必要信息的事件集。

调试器

插入断点

可以通过设置每个代码对象事件(LINEINSTRUCTION)并对所有不匹配断点的事件返回 DISABLE 来插入断点。

单步执行

调试器通常提供按单个指令或行单步执行的功能。

像断点一样,单步执行可以通过设置每个代码对象的事件来实现。一旦恢复正常执行,就可以取消设置本地事件。

附加

调试器可以使用 PY_STARTPY_RESUME 事件来得知何时首次遇到代码对象,以便可以插入任何必要的断点。

覆盖率工具

覆盖率工具需要跟踪控制图的哪些部分已执行。为此,它们需要注册 PY_ 事件以及 JUMPBRANCH

然后,这些信息可以在执行完成后转换回基于行的报告。

分析器

简单的分析器需要收集有关调用的信息。为此,分析器应注册以下事件

  • PY_START
  • PY_RESUME
  • PY_THROW
  • PY_RETURN
  • PY_YIELD
  • PY_UNWIND
  • CALL
  • C_RAISE
  • C_RETURN

基于行的分析器

基于行的分析器可以使用 LINEJUMP 事件。分析器实现者应该意识到,检测 LINE 事件将对性能产生巨大影响。

注意

插装分析器会产生显著的开销,并会扭曲分析结果。除非您需要精确的调用计数,否则请考虑使用统计分析器。

被拒绝的想法

本 PEP 的一个草案版本提议由用户负责插入监控指令,而不是由 VM 完成。然而,这给工具带来了太大的负担,并且会使附加调试器几乎不可能。

本 PEP 的早期版本建议将事件存储为 enums

class Event(enum.IntFlag):
    PY_START = ...

然而,这将阻止在 enum 模块加载之前监控代码,并可能导致不必要的开销。


来源:https://github.com/python/peps/blob/main/peps/pep-0669.rst

最后修改:2025-02-01 07:28:42 GMT