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

Python 增強提案 (Python Enhancement Proposals)

PEP 669 – CPython 的低影響監控

作者:
Mark Shannon <mark at hotpy.org>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2021年8月18日
Python 版本:
3.12
公告歷史:
2021年12月7日, 2022年1月10日
決議:
Discourse 訊息

目錄

重要資訊

本 PEP 為歷史文件。最新且標準的說明文件現已可於 sys.monitoring 找到。

×

關於如何提出變更建議,請參閱 PEP 1

摘要

在 CPython 中使用效能分析器或除錯器可能會對效能產生嚴重影響。效能下降一個數量級是很常見的。

本 PEP 提議為執行於 CPython 之上的 Python 程式設計一套監控 API,以實現低成本監控。

雖然本 PEP 未指定實作方式,但預期將使用 PEP 659 的加速 (quickening) 步驟來實作。

將會新增一個 sys.monitoring 命名空間,其中包含相關的函式與常數。

動機

開發者不應為了使用除錯器、效能分析器及其他類似工具而付出不合理的代價。

C++ 和 Java 開發者期望能夠在除錯器下以全速(或非常接近全速)執行程式。Python 開發者也應享有同樣的預期。

原理

PEP 659 提供的加速機制,提供了一種動態修改正在執行的 Python 位元組碼 (bytecode) 的方法。這些修改除了在被修改的程式碼部分之外,幾乎沒有額外成本,且對被修改的部分而言成本相對較低。我們可以利用這一點,提供一種在 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)(在生成前立即發生,被呼叫者的框架將在堆疊上)。
  • 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 事件控制。只有在監控相應的 CALL 事件時,才能看到 C_RETURNC_RAISE 事件。

其他事件

其他事件不一定與程式中的特定位置掛鉤,且無法個別停用。

可監控的其他事件包括:

  • PY_THROW
  • PY_UNWIND
  • RAISE
  • EXCEPTION_HANDLED

STOP_ITERATION 事件

PEP 380 規定在從產生器或協程返回一個值時,會引發 StopIteration 異常。然而,這是一種非常低效的返回數值方式,因此某些 Python 實作(特別是 CPython 3.12+)除非該異常對其他程式碼可見,否則不會引發異常。

為了允許工具在不拖慢產生器和協程的情況下監控真正的異常,提供了 STOP_ITERATION 事件。與 RAISE 不同,STOP_ITERATION 可以被本地停用。

工具識別碼

VM 一次最多支援 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) 啟用 codeevent_set 所設定的所有本地事件。若 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,則在呼叫 sys.monitoring.restart_events() 之前,該函式將不再針對該 (code, instruction_offset) 被呼叫。此功能是為了覆蓋率工具及其他只需查看一次事件的工具所提供。

請注意 sys.monitoring.restart_events() 並非針對單一工具,因此工具必須準備好接收它們選擇 DISABLE 的事件。

回呼函式中的事件

對於註冊了該回呼的工具而言,事件在回呼函式及其被呼叫者中是暫停的。

這意味著其他工具將會看到針對其他工具的回呼函式中所發生的事件。這對除錯效能分析工具可能很有用,但會產生誤導性的分析結果,因為除錯工具本身會出現在分析結果中。

事件順序

如果一條指令觸發了多個事件,它們會按以下順序發生:

  • LINE
  • INSTRUCTION
  • 所有其他事件(每條指令只能發生其中一個事件)

每個事件會按 ID 的升序傳遞給工具。

「呼叫」(call) 事件群組

大多數事件是獨立的;設定或停用一個事件對其他事件沒有影響。然而,CALL, C_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 的語意是 PEP 523 外掛開發者的責任。不改變 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,可以以極低的成本實作覆蓋率工具。

對於高度檢測 (instrumented) 的程式碼(例如使用 LINE),效能應優於 sys.settrace,但不會好太多,因為效能將受限於回呼中所花費的時間。

對於最佳化虛擬機器,例如未來版本的 CPython(以及 PyPy 若選擇支援此 API),在長執行程式的中途更改活動事件集合可能相當昂貴,可能會花費數百毫秒,因為這會觸發去最佳化 (de-optimization)。一旦發生此類去最佳化,隨著 VM 重新最佳化被檢測程式碼,效能應會恢復。

一般而言,這些操作可以視為快速的

  • 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
None ≈40% ≈80%
兩個或以上 ≈40% ≈120% ≈200%

安全性影響

允許修改正在執行的程式碼確實有一些安全性影響,但不會超過產生和呼叫新程式碼的能力。

上述所有新函式都將觸發審計掛鉤 (audit hooks)。

實作

本文概述了 CPython 3.12 的提議實作。CPython 後續版本及其他 Python 實作的實際實作可能會大相徑庭。

本 PEP 的提議實作將建立在 CPython 3.11 的加速步驟之上,如 PEP 659 所述。檢測 (Instrumentation) 的運作方式與加速非常相似,位元組碼會根據需要被替換為已檢測的位元組碼。

例如,如果啟用了 CALL 事件,那麼所有的呼叫指令都會被替換為 INSTRUMENTED_CALL 指令。

請注意,這會干擾最佳化,除了呼叫已註冊的可呼叫物件之開銷外,還會導致某些效能下降。

當活動事件集合發生變化時,VM 將立即更新任何執行緒堆疊上的所有程式碼物件。它還會設定陷阱 (traps) 以確保所有程式碼物件在被呼叫時能正確地進行檢測。因此,更改活動事件集合的頻率應盡可能低,因為這可能是一個非常昂貴的操作。

其他事件(如 RAISE)可以廉價地開啟或關閉,因為它們不依賴程式碼檢測,而是在底層事件發生時進行執行階段檢查。

需要檢測的確切事件集合屬於實作細節,但對於目前的設計,以下事件將需要檢測:

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

每個已檢測的位元組碼將需要額外的 8 位元資訊來註明該檢測適用於哪個工具。LINEINSTRUCTION 事件需要額外資訊,因為它們需要儲存原始指令,甚至在與其他檢測重疊時儲存已檢測的指令。

實作工具

本 PEP 的理念是:第三方監控工具應該能夠實現高效能,而非實作起來很容易。

將事件轉換為對使用者有意義的數據,是工具本身的責任。

所有事件都有成本,工具應嘗試使用觸發頻率最低但仍能提供必要資訊的事件組合。

除錯器

插入斷點

斷點可以透過設定每個程式碼物件的事件(LINEINSTRUCTION)來插入,並對任何不符合斷點的事件回傳 DISABLE

單步執行

除錯器通常提供單步執行指令或行的功能。

與斷點一樣,單步執行可以透過設定每個程式碼物件的事件來實現。一旦需要恢復正常執行,本地事件即可取消設定。

附加

除錯器可以使用 PY_STARTPY_RESUME 事件,在首次遇到程式碼物件時獲得通知,以便插入任何必要的斷點。

覆蓋率工具

覆蓋率工具需要追蹤控制圖的哪些部分已被執行。為此,它們需要註冊 PY_ 事件,以及 JUMPBRANCH

這些資訊可以在執行完成後轉換為基於行的報告。

效能分析器 (Profiler)

簡單的效能分析器需要收集有關呼叫的資訊。為此,效能分析器應註冊以下事件:

  • 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年2月1日 07:28:42 GMT