Cinder 是 Meta 內部以效能為導向的 CPython 3.10 生產版本。它包含許多效能優化,包括字節碼內聯快取、協程的即時評估、一次方法JIT 以及一個實驗性字節碼編譯器,該編譯器使用類型註釋來發出在JIT 中性能更好的類型專用字節碼。
Cinder 為 Instagram 提供支持,並且越來越多地在 Meta 中越來越多的 Python 應用程式中使用。
有關 CPython 的更多信息,請參閱README.cpython.rst
。
簡短的回答:不。
我們公開 Cinder 是為了促進可能將部分工作上游到 CPython 的討論,並減少致力於 CPython 性能的人員之間的重複工作。
Cinder 並未經過完善或記錄以供其他人使用。我們不希望它成為 CPython 的替代品。我們提供此程式碼的目標是統一更快的 CPython。因此,雖然我們確實在生產環境中運行 Cinder,但如果您選擇這樣做,您就得靠自己了。我們無法承諾修復外部錯誤報告或審查拉取請求。我們確保 Cinder 對於我們的生產工作負載來說足夠穩定和快速,但我們不保證其對於任何外部工作負載或用例的穩定性、正確性或效能。
也就是說,如果您有動態語言運行時的經驗並且有想法讓 Cinder 更快;或者,如果您從事 CPython 工作並希望使用 Cinder 作為改進 CPython 的靈感(或幫助 Cinder 的上游部分遷移到 CPython),請聯絡我們;我們很樂意聊天!
Cinder 應該像 CPython 一樣建構; configure
並make -j
。然而,由於 Cinder 的大多數開發和使用都發生在 Meta 的高度特定上下文中,因此我們不會在其他環境中進行太多練習。因此,建置和運行 Cinder 最可靠的方法是重複使用 GitHub CI 工作流程中基於 Docker 的設定。
如果您只是想獲得一個可以工作的 Cinder 而不想自己構建它,我們的運行時 Docker 映像將是最簡單的(不需要存儲庫克隆!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
如果您想自己建造:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
請注意,Cinder 僅在 Linux x64 上建置或測試;其他任何東西(包括 macOS)可能都不起作用。上面的 Docker 映像是基於 Fedora Linux,並根據 Cinder 儲存庫中的 Docker 規格檔案建置: .github/workflows/python-build-env/Dockerfile
。
有一些可能有趣的新測試目標。 make testcinder
與make test
幾乎相同,只是它跳過了一些在我們的開發環境中存在問題的測試。 make testcinder_jit
在完全啟用 JIT 的情況下運行測試套件,因此所有函數都是 JIT 的函數。 make testruntime
為 JIT 執行一組 C++ gtest 單元測試。並使make test_strict_module
運行嚴格模組的測試套件(見下文)。
請注意,這些步驟產生的 Cinder Python 二進位檔案未啟用 PGO/LTO 最佳化,因此不要指望使用這些指令來對任何 Python 工作負載進行任何加速。
Cinder Explorer 是一個即時遊樂場,您可以在其中看到 Cinder 如何將 Python 程式碼從原始碼編譯到組件——歡迎您嘗試!請隨意提交功能請求和錯誤報告。請記住,Cinder Explorer 與其他部分一樣,會盡最大努力「提供支援」。
Instagram 使用多進程網路伺服器架構;父進程啟動,執行初始化工作(例如載入程式碼),並分叉數十個工作進程來處理客戶端請求。由於多種原因(例如記憶體洩漏、程式碼部署),工作進程會定期重新啟動,且其生命週期相對較短。在此模型中,當修改物件的參考計數時,作業系統必須複製包含在父行程中指派的物件的整個頁面。實際上,父進程中分配的物件比工作進程的壽命長。所有與引用計數相關的工作都是不必要的。
Instagram 擁有非常龐大的 Python 程式碼庫,並且由於引用計數長壽命物件的寫入時複製而產生的開銷非常大。我們開發了一種名為「不朽實例」的解決方案,以提供一種從引用計數中選擇退出物件的方法。有關詳細信息,請參閱 Include/object.h。此功能透過定義 Py_IMMORTAL_INSTANCES 進行控制,並且在 Cinder 中預設為啟用。這對我們在生產中來說是一個巨大的勝利(~5%),但它使直線程式碼變慢。引用計數操作頻繁發生,啟用此功能時必須檢查物件是否參與引用計數。
“影子代碼”或“影子字節碼”是我們的專用解釋器的實作。它觀察通用 Python 操作碼執行中的特定可最佳化情況,並(對於熱函數)動態地將這些操作碼替換為專用版本。 Shadowcode 的核心位於Shadowcode/shadowcode.c
中,儘管專用字節碼的實作與 eval 循環的其餘部分一起位於Python/ceval.c
中。 Shadowcode 特定的測試位於Lib/test/test_shadowcode.py
中。
它在本質上與 CPython 3.11 中內建的專用自適應解釋器 (PEP-659) 類似。
Instagram 伺服器是一個非同步的工作負載,其中每個 Web 請求可能會觸發數十萬個非同步任務,其中許多任務可以在不暫停的情況下完成(例如,借助記憶值)。
我們擴展了向量呼叫協定以傳遞一個新標誌Ci_Py_AWAITED_CALL_MARKER
,指示呼叫者正在立即等待此呼叫。
當與立即等待的非同步函數呼叫一起使用時,我們可以立即(熱切地)評估被呼叫的函數,直到完成或第一次掛起。如果函數在沒有掛起的情況下完成,我們就可以立即傳回值,而無需額外的堆分配。
當與非同步收集一起使用時,我們可以立即(熱切地)評估傳遞的可等待集合,從而可能避免為可以同步完成的協程、已完成的 future、記憶值等創建和調度多個任務的成本。
這些最佳化導致 CPU 效率顯著提高 (~5%)。
這主要在Python/ceval.c
中透過新的向量呼叫標誌Ci_Py_AWAITED_CALL_MARKER
實現,指示呼叫者正在立即等待此呼叫。尋找IS_AWAITED()
巨集和此向量呼叫標誌的使用。
Cinder JIT 是用 C++ 實作的一次方法自訂 JIT。它是透過-X jit
標誌或PYTHONJIT=1
環境變數啟用的。它支援幾乎所有Python操作碼,並且可以在許多Python性能基準測試中實現1.5-4倍的速度提升。
預設情況下,啟用後,它將對曾經調用的每個函數進行 JIT 編譯,由於 JIT 編譯很少調用的函數的開銷,這很可能會使您的程式變慢,而不是更快。選項-X jit-list-file=/path/to/jitlist.txt
或PYTHONJITLISTFILE=/path/to/jitlist.txt
可以將其指向包含完全限定函數名稱的文字檔案(格式為path.to.module:funcname
或path.to.module:ClassName.method_name
),每行一個,應該是 JIT 編譯的。我們使用此選項僅編譯一組從生產分析資料派生的熱函數。 (JIT 的一種更典型的方法是動態編譯函數,因為我們觀察到它們被頻繁地呼叫。對於我們來說,實現這一點還不值得,因為我們的生產架構是一個預先分叉的網路伺服器,而且對於由於記憶體共享原因,我們希望在工作進程分叉之前在初始進程中預先進行所有 JIT 編譯,這意味著在決定要 JIT 編譯哪些函數之前我們無法觀察進程中的工作負載。
JIT 位於Jit/
目錄中,其 C++ 測試位於RuntimeTests/
中(使用make testruntime
運行它們)。 Lib/test/test_cinderjit.py
中也有一些 Python 測試;這些並不意味著詳盡無遺,因為我們透過make testcinder_jit
在 JIT 下運行整個 CPython 測試套件;它們涵蓋了 CPython 測試套件中未發現的 JIT 邊緣情況。
有關影響 JIT 行為的其他一些-X
選項和環境變量,請參閱Jit/pyjit.cpp
。文件中也定義了一個cinderjit
模組,它將一些 JIT 實用程式公開給 Python 程式碼(例如,強制編譯特定函數、檢查函數是否已編譯、停用 JIT)。請注意, cinderjit.disable()
僅禁用將來的編譯;它立即編譯所有已知函數並保留現有的 JIT 編譯函數。
JIT 首先將 Python 字節碼降低為高階中間表示(HIR);這是在Jit/hir/
中實現的。 HIR 與Python 字節碼的映射相當接近,儘管它是寄存器機而不是堆疊機,但它的級別要低一些,它是類型化的,並且一些被Python 字節碼掩蓋但對性能很重要的細節(尤其是引用計數)是在 HIR 中明確暴露。 HIR被轉換為SSA形式,對其執行一些最佳化,然後根據有關HIR操作碼的引用計數和記憶體影響的元資料自動將引用計數操作插入其中。
然後,HIR 被降低為低階中間表示 (LIR),這是對彙編的抽象,在Jit/lir/
中實現。在 LIR 中,我們進行暫存器分配、一些額外的最佳化,最後使用優秀的 asmjit 函式庫將 LIR 降級為彙編(在Jit/codegen/
中)。
JIT 尚處於早期階段。雖然它已經可以消除解釋器循環開銷並為許多函數提供顯著的性能改進,但我們才剛開始觸及可能優化的表面。許多常見的編譯器最佳化尚未實作。我們的最佳化優先順序很大程度上是由 Instagram 製作工作負載的特徵決定的。
嚴格的模組是將一些東西合而為一:
1. 靜態分析器能夠驗證執行模組的頂級程式碼不會在該模組外部產生可見的副作用。
2. 不可變的StrictModule
類型可用來取代 Python 的預設模組類型。
3. 一個 Python 模組載入器,能夠識別選擇嚴格模式的模組(透過模組頂部的import __strict__
),分析它們以驗證沒有導入副作用,並將它們作為StrictModule
物件填充到sys.modules
中。
靜態 Python 是一種字節碼編譯器,它利用類型註解來產生類型專用且經過類型檢查的 Python 字節碼。與 Cinder JIT 一起使用,它在許多情況下可以提供類似於 MyPyC 或 Cython 的效能,同時提供純 Python 開發人員體驗(正常的 Python 語法,無需額外的編譯步驟)。靜態 Python 加上 Cinder JIT 在 Richards 基準測試的類型化版本上實現了普通 CPython 18 倍的效能。在 Instagram,我們已成功在生產中使用靜態 Python 來替換我們主要網頁伺服器程式碼庫中的所有 Cython 模組,並且沒有出現效能下降。
靜態Python編譯器建構在Python compiler
模組之上,該模組在Python 3中從標準函式庫中刪除,此後一直在外部進行維護和更新;該編譯器已合併到Lib/compiler
中的 Cinder 中。靜態Python編譯器在Lib/compiler/static/
中實現,其測試在Lib/test/test_compiler/test_static.py
中。
靜態 Python 模組中定義的類別會自動給出類型化槽(基於對其類型化類別屬性的檢查和__init__
中帶註釋的賦值),並且針對這些類型的實例的屬性加載和存儲使用新的STORE_FIELD
和LOAD_FIELD
操作碼,這些操作碼在JIT 中變成直接從物件中的固定記憶體偏移量載入/儲存到物件中,沒有LOAD_ATTR
或STORE_ATTR
的間接尋址。類別也獲得其方法的虛擬函數表,供下面提到的INVOKE_*
操作碼使用。這些功能的執行階段支援位於StaticPython/classloader.h
和StaticPython/classloader.c
中。
靜態 Python 函數以隱藏的序言開頭,它檢查提供的參數類型是否與類型註解匹配,如果不匹配則引發TypeError
。從靜態 Python 函數到另一個靜態 Python 函數的呼叫將跳過此操作碼(因為編譯器已驗證類型)。靜態到靜態呼叫還可以避免典型 Python 函數呼叫的大部分開銷。我們發出一個INVOKE_FUNCTION
或INVOKE_METHOD
操作碼,其中攜帶有關被呼叫函數或方法的元資料;加上可選的不可變模組(透過StrictModule
)和類型(透過cinder.freeze_type()
,我們目前將其應用於導入載入器中嚴格和靜態模組中的所有類型,但將來可能會成為靜態Python 的固有部分)並編譯被調用者簽名的實時知識使我們能夠(在 JIT 中)將許多 Python 函數調用轉換為使用 x64 調用約定對固定內存地址的直接調用,而開銷只比 C 函數調用多一點。
靜態Python仍然是逐漸類型化的,並且透過回退到正常的Python動態行為來支援僅部分註釋或使用未知類型的程式碼。在某些情況下(例如,從帶有傳回註解的函數傳回靜態未知類型的值時),會插入執行時間CAST
操作碼,如果執行時間類型與預期類型不匹配,則會引發TypeError
。
靜態 Python 也支援機器整數、布林值、雙精度數和向量/陣列的新類型。在 JIT 中,這些值被處理為未裝箱的值,例如原始整數算術避免了所有 Python 開銷。內建類型(例如清單或字典下標或len()
)的一些操作也進行了最佳化。
Cinder 透過嚴格/靜態模組載入器支援逐步採用靜態模組,該載入器可以自動偵測靜態模組並透過跨模組編譯將其載入為靜態模組。載入器將在檔案頂部尋找import __static__
和import __strict__
註釋,並適當地編譯模組。若要啟用載入程序,您可以選擇以下三個選項之一:
1. 透過from cinderx.compiler.strict.loader import install; install()
。
PYTHONINSTALLSTRICTLOADER=1
。./python -X install-strict-loader application.py
。或者,您可以使用./python -m compiler --static some_module.py
靜態編譯所有程式碼,這會將模組編譯為靜態 Python 並執行它。
有關更詳細的文檔,請參閱CinderDoc/static_python.rst
。