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

Python 增強提案 (Python Enhancement Proposals)

PEP 709 – 內聯推導式

作者:
Carl Meyer <carl at oddbird.net>
贊助人:
Guido van Rossum <guido at python.org>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2023年2月24日
Python 版本:
3.12
公告歷史:
2023年2月25日
決議:
Discourse 訊息

目錄

摘要

目前的推導式被編譯為巢狀函式,這能隔離推導式的迭代變數,但在執行時期(runtime)效率低下。本 PEP 提議將列表、字典和集合推導式「內聯」(inline)到定義它們的程式碼中,並透過在堆疊上推入/彈出衝突的區域變數來提供預期的隔離效果。此變更大幅提升了推導式的執行速度:在僅針對推導式的微基準測試中速度提升高達 2 倍;而在一個源自實際程式碼、且大量使用推導式來進行實際運算的基準測試中,則有 11% 的效能提升。

動機

推導式是 Python 語言中廣受歡迎且被廣泛使用的功能。推導式的巢狀函式編譯方式,是為了編譯器的簡易性而犧牲使用者程式碼的效能。我們可以在僅增加少量編譯器複雜度的情況下,提供幾乎相同的語意(參見向後相容性),同時為所有推導式使用者帶來更好的執行時期效能。

原理

內聯是許多程式語言中常見的編譯器最佳化技術。在 Python 中,要在編譯時期對函式呼叫進行通用內聯幾乎是不可能的,因為呼叫目標可能會在執行時期被修補(patched)。推導式是一個特殊情況,因為我們在編譯器中靜態已知呼叫目標,且該目標既無法被修補(除非對位元組碼進行未記載且不被支援的直接篡改),也無法逃逸。

內聯也允許位元組碼的其他編譯器最佳化技術發揮更大效用,因為它們現在可以「看穿」推導式的位元組碼,而不僅僅將其視為一個不透明的呼叫。

通常效能提升不需要通過 PEP。但在本案中,最簡單且最高效的實作會產生一些使用者可見的影響,因此這不僅僅是效能改進,更是一項(微小的)語言變更。

規範

給定一個簡單的推導式

def f(lst):
    return [x for x in lst]

編譯器目前為函式 f 產生以下位元組碼

1           0 RESUME                   0

2           2 LOAD_CONST               1 (<code object <listcomp> at 0x...)
            4 MAKE_FUNCTION            0
            6 LOAD_FAST                0 (lst)
            8 GET_ITER
           10 CALL                     0
           20 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x...>:
2           0 RESUME                   0
            2 BUILD_LIST               0
            4 LOAD_FAST                0 (.0)
      >>    6 FOR_ITER                 4 (to 18)
           10 STORE_FAST               1 (x)
           12 LOAD_FAST                1 (x)
           14 LIST_APPEND              2
           16 JUMP_BACKWARD            6 (to 6)
      >>   18 END_FOR
           20 RETURN_VALUE

推導式的位元組碼位於一個獨立的程式碼物件(code object)中。每次呼叫 f() 時,都會配置(透過 MAKE_FUNCTION)一個新的單次使用函式物件,進行呼叫(在 Python 堆疊上配置然後銷毀一個新框架),然後立即丟棄。

根據此 PEP,編譯器將為 f() 產生以下位元組碼來取代上述行為

1           0 RESUME                   0

2           2 LOAD_FAST                0 (lst)
            4 GET_ITER
            6 LOAD_FAST_AND_CLEAR      1 (x)
            8 SWAP                     2
           10 BUILD_LIST               0
           12 SWAP                     2
      >>   14 FOR_ITER                 4 (to 26)
           18 STORE_FAST               1 (x)
           20 LOAD_FAST                1 (x)
           22 LIST_APPEND              2
           24 JUMP_BACKWARD            6 (to 14)
      >>   26 END_FOR
           28 SWAP                     2
           30 STORE_FAST               1 (x)
           32 RETURN_VALUE

不再有獨立的程式碼物件,也不再建立單次使用的函式物件,亦無需建立和銷毀 Python 框架。

迭代變數 x 的隔離是透過在偏移量 6 處結合新的 LOAD_FAST_AND_CLEAR 操作碼(在執行推導式前將 x 的任何外部值儲存於堆疊上)以及偏移量 30STORE_FAST(在推導式執行後恢復 x 的外部值,若有的話)來實現的。

如果推導式存取外部作用域的變數,內聯避免了將這些變數放入單元(cell)的需求,允許推導式(以及外部函式中的所有其他程式碼)將它們當作正常的快速區域變數(fast locals)來存取。這提供了進一步的效能提升。

在某些情況下,推導式的迭代變數可能是外部作用域中的全域變數、單元變數(cellvar)或自由變數(freevar),而非簡單的函式區域變數。在這些情況下,編譯器也會在進入/離開推導式時,在內部推入並彈出該變數的作用域資訊,以維護語意。例如,如果該變數在推導式外是全域變數,在推導式外參照該處時仍會使用 LOAD_GLOBAL,但在推導式內則會使用 LOAD_FAST / STORE_FAST。如果它是外部的單元變數/自由變數,用於儲存/恢復它的 LOAD_FAST_AND_CLEAR / STORE_FAST 並不會改變(因為不存在 LOAD_DEREF_AND_CLEAR),這意味著整個單元(而不僅僅是其中的值)會被儲存/恢復,因此推導式不會寫入外部的單元中。

發生在模組或類別作用域中的推導式也會被內聯。在這種情況下,推導式僅會在推導式內部為迭代變數引入快速區域變數(LOAD_FAST / STORE_FAST)的使用,而在原本僅使用 LOAD_NAME / STORE_NAME 的作用域中,以此維持隔離性。

實際上,推導式引入了一個子作用域,其中的區域變數是完全隔離的,但不會產生呼叫所需的效能成本或堆疊框架建立成本。

產生器表達式(Generator expressions)在目前本 PEP 的參考實作中未被內聯。未來某些不洩漏回傳產生器物件的產生器表達式可能會被內聯。

非同步推導式(Asynchronous comprehensions)的內聯方式與同步推導式相同;無需特殊處理。

回溯相容性

推導式內聯將導致以下可見的行為變更。為了適應實作中的這些變化,標準函式庫或測試套件無需進行任何更改,這顯示其對使用者程式碼的影響極小。

依賴編譯器位元組碼輸出之未記載細節的專業工具,當然可能會受到上述範圍之外的影響,但這些工具本就必須適應每個 Python 版本的位元組碼變更。

locals() 包含外部變數

在推導式內部呼叫 locals() 將包含該包含推導式之函式的所有區域變數。例如,給定以下函式

def f(lst):
    return [locals() for x in lst]

在目前的 Python 中呼叫 f([1]) 將回傳

[{'.0': <list_iterator object at 0x7f8d37170460>, 'x': 1}]

其中 .0 是一個內部的實作細節:即推導式「函式」的合成唯一參數。

根據此 PEP,它將改為回傳

[{'lst': [1], 'x': 1}]

這現在將外部的 lst 變數包含為區域變數,並消除了合成的 .0

回溯追蹤(tracebacks)中不再出現推導式框架

根據此 PEP,推導式將不再在堆疊追蹤中擁有自己專用的框架。例如,給定此函式

def g():
    raise RuntimeError("boom")

def f():
    return [g() for x in [1]]

目前,呼叫 f() 會產生以下追蹤結果

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in f
  File "<stdin>", line 5, in <listcomp>
  File "<stdin>", line 2, in g
RuntimeError: boom

請注意 <listcomp> 的專用框架。

根據此 PEP,追蹤結果看起來如下

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in f
  File "<stdin>", line 2, in g
RuntimeError: boom

列表推導式不再有額外的框架。不過,f 函式的框架具有推導式的正確行號,因此這只是使追蹤結果更精簡,而不會遺失任何有用的資訊。

理論上,使用帶有 stacklevel 參數的警告(warnings)的程式碼,可能會因為框架堆疊的變更而觀察到行為變化。然而在實務上,這種情況不太可能發生。這需要一個在函式庫程式碼中觸發的警告,且該警告總是透過同一個函式庫中的推導式進行呼叫,並使用 3 或以上的 stacklevel 來跳過推導式及其包含函式,以指向該函式庫之外的呼叫框架。在此種場景下,通常在更接近呼叫程式碼的地方發出警告並跳過更少的框架會更簡單且更可靠。

追蹤/效能分析(Tracing/profiling)將不再顯示推導式的呼叫/返回

自然地,由於列表/字典/集合推導式將不再實作為對巢狀函式的呼叫,使用 sys.settracesys.setprofile 進行的追蹤/效能分析,也將不再反映出呼叫與返回的過程。

對其他 Python 實作的影響

根據 GraalPythonPyPy 代表的評論,鑑於某些人可能會在某個時刻依賴這些可觀察的行為,他們可能覺得需要適應此處的變更。因此,在其他條件相同的情況下,較少的可觀察變更意味著較少的工作量。但這些變更(至少對 GraalPython 而言)應該是「無需太多頭痛」就能處理的。

如何教學

推導式語法會導致建立並呼叫巢狀函式,這並非直覺顯而易見的。對於尚未習慣先前行為的新使用者,我懷疑本 PEP 中的新行為會更直覺,且需要更少的解釋。(「當我沒有定義任何這樣的函式時,為什麼我的追蹤中有 <listcomp> 行?我在 locals() 中看到的 .0 變數是什麼?」)

安全性影響

目前已知無。

參考實作

本 PEP 已有一個參考實作,以 針對 CPython 主分支的 PR 形式存在,並通過了所有測試。

該參考實作在進行微基準測試 ./python -m pyperf timeit -s 'l = [1]' '[x for x in l]' 時,比 main 分支快 1.96 倍(在以 --enable-optimizations 編譯的建置中)。

該參考實作在 pyperformance 基準測試套件中執行 comprehensions 測試(這不是單純推導式的微基準測試,而是測試大量使用推導式進行實際工作的真實程式碼)時,比 main 分支快 11%(同樣在最佳化建置中)。pyperformance 中的其他基準測試(皆未大量使用推導式)未顯示出超出雜訊範圍的影響。

此實作對非推導式的程式碼沒有影響。

否決的想法

更高效的推導式呼叫,無需內聯(Inlining)之外的方法

另一種 替代方案 引入了一種新的操作碼,以精簡方式「呼叫」推導式,無需建立拋棄式函式物件,但仍會建立新的 Python 框架。這避免了所有在 向後相容性 下列出的可見影響,並提供了大約一半的效能優勢(在微基準測試上有 1.5 倍的改善,在 pyperformance 的 comprehensions 基準測試上有 4% 的改善)。它還需要在 _PyInterpreterFrame 結構體中新增一個指標,並在每次框架建構時進行一次新的 Py_INCREF,這意味著(與本 PEP 不同)它對所有程式碼都有(非常微小的)效能代價。它也為未來最佳化提供的空間較少。

本 PEP 的立場是,完全內聯所帶來的額外效能提升,足以證明行為變更是合理的。


來源:https://github.com/python/peps/blob/main/peps/pep-0709.rst

最後修改:2023-12-15 15:06:12 GMT