Python爲什麽如此緩慢

程序員咋不禿頭 2024-06-21 10:03:58

導讀:在 PyCon 2024 大會上,部分技術專家展示了加速 Python 編程語言的多種方法,包括子解釋器、永久對象、即時編譯等。

有一句“古老”的 Python 格言:

“使用 Python,您需要在運行時付費”。

是的,Python 語言因其運行緩慢而“聲名狼藉”,但它卻是一種很好的入門語言,雖然速度不如其更複雜的同類編程語言。

好的消息是,上個月在匹茲堡舉行的PyCon US 2024上的許多演講都展示了研究人員如何推動該語言的前沿技術升級以及發展。

編譯 Python 代碼加快數學運算速度

Saksham Sharma 在 Tower Research Capital 擔任量化研究技術總監,但是他使用 C++ 構建交易系統。

他在演講時說,“我喜歡快速的代碼”,他坦承表示希望將這種熱情帶到 Python 中。

Saksham Sharma 出席 2024 年 PyCon大會

Python 是一種解釋型語言,但是 Python 本身的CPython 參考,實際上的實現是用 C 編寫的。

解釋器將源代碼轉換爲由 1 和 0 組成的高效字節碼。然後它直接執行源代碼,並在讀入所有對象和變量時根據這些對象和變量構建程序的內部狀態,而不是像編譯器那樣提前編譯成機器碼。

Sharma 說:“所以,我們在這裏經曆了一系列的間接過程,所以事情可能會變得緩慢。”

對于 Python 來說,即使是將兩個數字相加的簡單指令,也會導致對 CPU 本身産生超過 500 條指令,不僅包括加法本身,還包括所有支持指令,例如將答案寫入新對象。

一款新編譯器 Cython是一個針對 Python 進行優化的靜態編譯器,它允許您用 C 編寫代碼,提前編譯,然後在 Python 程序中使用結果。

Sharma 提示說:

“你可以構建內置于解釋器中的外部庫和實用程序,它們可以與解釋器的內部狀態進行交互,如果你在解釋器上編寫了一個函數,則可以配置爲調用該函數。”

例如, Python 代碼的兩個變量相加:

#Code written by Saksham Sharmadef print_add(a, b)c = a + bprint(c)return c

可以這樣爲 Cython 呈現:

#Code written by Saksham SharmaPyObject *__pyx_pf_14cython_binding_print_add(...) {...__Pyx_XDECREF(__pyx_r);__Pyx_INCREF(__pyx_v_c);__pyx_r = __pyx_v_c;goto _pyx_L0;...}

代碼側開發需要輸入更多內容,但編譯器側的工作量會減少。

Sharma 發現,在自己的機器上,此種額外操作使用 Python 大概需要 70 納秒,但使用 Cython 只需要大概 14 納秒。

“Cython 確實讓速度更快了,因爲解釋器不再起作用。例如,每次解釋器進行兩個變量的相加時,它都必須檢查每個變量的類型。但如果你已經知道類型是什麽,爲什麽不取消這種檢查呢?這就是程序員在代碼中聲明變量類型時所做的。

Sharma 說:“這樣輸入的代碼可以快得多。”

Cython 可以加速內循環

使用靜態類型進行縮放的 Python

正如 Sharma 演講中所指出的那樣,通過 Python 中的靜態類型化可以獲得很多優勢。使用靜態類型化,您可以定義變量的數據類型。它是字符串?整數?數組?解釋器需要時間來弄清楚所有這些。

Anaconda 首席軟件工程師Antonio Cuni介紹了SPy,這是 Python 的一個新子集,需要靜態類型。其目的是提供 C 或 C++ 的速度,同時保留 Python 本身的用戶友好感。

Cuni 解釋說,Python 在執行指令之前必須做很多事情。與 Sharma 一樣,Cuni 指出,“使用低級語言時,運行時通常要做的工作比較少。”

在執行邏輯之前,Python 解釋器必須找到所有工具(如工具和庫)來執行邏輯本身,這個中間階段的工作會花費大量時間。

好消息是,很多工作可以在編譯階段提前完成。

使用 SPy,所有全局常量(例如類、模塊和全局數據)都被凍結爲“不可變的”,並且可以使用即時 (JIT) 編譯器進行優化,得益于它們的類型。

目前,Cuni 正致力于實現 SPy,要麽作爲 CPython 的擴展,要麽使用自己的 JIT 編譯器。

此外,Cuni 表示,他還在研究可以在WebAssembly中運行的版本。

圖編譯與解釋 (Antonio Cuni)

靜態鏈接的 C 擴展

Meta 工程經理Loren Arthur在演講中表示,用 C 重寫處理繁重的函數可以大大提高性能。但是,您必須小心將它們加載到程序中的方式。

在他自己的演示中,導入 Python 的 AC 模塊可以將樣本文件中數據的處理時間從 4 秒縮短到近半秒(常規 Python 代碼的處理時間)。

這聽起來微不足道。但對于Meta 這樣規模的處理來說,這是一筆不小的開銷。將 Python 功能轉換爲更靈活的 C 代碼,用于 90,000 個 Python 應用程序,僅憑構建速度的提高,Meta 工程師每周就能節約 5,000 小時。

沒錯,這太棒了。然後 Instagram 就構建了數千個 C 擴展來推動事情的發展。

但是後來這家社交媒體巨頭又遭遇到了另一個問題。構建中包含的 C 擴展越多,導入時間開始變長。這很令人奇怪,因爲大多數模塊都很小,可能只有一個方法和一個字符串返回值。

在使用Callgrind時(它是Valgrind動態分析工具套件的一部分),Arthur 發現一個名爲dlopen的 Python 函數打開共享對象時耗費了 92% 的時間。

他說道:“加載共享對象的代價很高,特別是當數量非常大的時候。”

Meta 以嵌入式 C 擴展的形式找到了答案,即對共享對象進行靜態鏈接而不是動態鏈接。C 代碼直接複制到可執行文件中,而不是調用共享對象。

永生的對象

全局解釋器鎖( GIL) 可以鎖定多個進程同時執行 Python 代碼,但在Azion Technologies 軟件工程師團隊負責人 Vinícius Gubiani Ferreira 看來,它並不是這個故事中的罪魁禍首。

相反地,GIL 的確是一位英雄,但由于存在時間太長而變成了“惡棍”。

Ferreira 的演講中討論了 PEP 683,旨在改善大型應用程序的內存消耗。最終的庫被納入10 月份發布的 Python v 3.12 中。

GIL 的設計初衷是防止競爭條件産生,但它後來也阻礙了 Python 進行真正的多核並行計算。目前,Python 正在努力使 GIL 功能成爲可選項,但可能還需要幾年時間才能將其穩定地融入語言運行時。

Ferreira 解釋說,基本上Python 中的一切都是對象。包括變量、字典、函數、方法甚至實例:所有都是對象。最基本的形式是,對象由類型、變量和引用計數組成,引用計數用于統計指向此對象的數量。

所有 Python 對象都是可變的,即使是那些標記爲不可變的對象(例如字符串)。

而且在 Python 中,引用計數會經常發生變化,這實際上是有問題的,因爲每次更新都意味著緩存會重新生成,基本上無效。它使分布式程序變得複雜。它會導致數據競爭,更改可能會相互覆蓋,如果結果等于零,那麽 Boom!垃圾收集器會立即刪除該對象。

如果應用程序的擴展越大,這些問題就愈加嚴重。

答案很簡單:創建一個引用計數爲永不可變狀態,即通過將引用計數設置爲無法更改的特定高數字(Ferreira 指出,你可以讓程序增加到該數字,這需要幾天時間)。運行時將單獨管理這些超級特殊對象,並負責關閉它們。

一個更好的特性是:這些特性還繞過了 GIL。因此,它們可以在任何場合使用,並且可以同時由多個線程使用。

使用此方法, Cython的性能會略有下降,最高可達 88% ,因爲考慮到運行時必須保留單獨的表,這並不奇怪。但特別是在多處理器環境(比如 Instagram)中,性能改進是值得的。

Ferreira 如此提示道:“你需要衡量一下,看看自己做地是否正確。”

共享不可變內容

繞過 GIL 的另一種方法是使用子解釋器,這也是今年PyCon的一個熱門話題。

子解釋器架構允許多個解釋器共享相同的內存空間,每個解釋器都有自己的 GIL。

一個名爲“memhive”的 Python 框架提供了這樣的一個編排器,它實現了一個子解釋器工作池以及一個 RPC 機制,以便它們可以共享數據。

它的創建者Yury Selivanov是 Python 核心開發人員和EdgeDB的首席執行官/聯合創始人,他也在 Pycon 演講中介紹了它。

Selivanov 首先在筆記本電腦上演示了一個程序,該程序使用 10 個 CPU 內核同時執行 10 個異步事件循環。它們共享相同的內存空間,即一百萬個key的內存空間。

是什麽阻止你在自己的機器上執行此操作?那個“老惡棍”——GIL。

Memhive 設置了一個主子解釋器,然後可以根據需要生成任意數量的其它子解釋器。

不可變對象是一個挑戰,Python 中有很多這樣的對象,例如字符串或元組。如果要更改它們,您必須創建一個新對象並複制每個元素。從計算上講,這是一個非常昂貴的操作,如果再考慮到更新緩存,則成本會加倍。

Memhive 使用一種共享數據結構,稱爲結構共享(隱藏在Python 庫中的hamt.c),其中捕獲後續更改,但引用而不是複制舊的不可變數據結構的部分,從而節省了大量工作。

共享參考結構化信息而不再複制副本,從而有效節省時間。

Selivanov 說:

“如果你想添加一個鍵,不必複制整個樹,你只需創建缺失的新分支,並引用其他分支,所以如果你有一個包含數十億個鍵的集合,並且你想添加一個新鍵,你只需創建幾個底層節點,其余的節點就可以重複使用。你不需要對它們做任何事情。”

結構化共享爲並行處理打開了大門,因爲數據是不可變的,從而允許多個子解釋器在同一數據集上並行工作。

“因爲我們使用的是不可變的東西,所以我們實際上可以安全地訪問底層內存,而無需獲取鎖或任何東西,”他說。這可以將速度提高 6 倍到 150,000 倍,具體取決于複制的數量。

即使變更的數量急劇增加,變更所需的時間仍在控制之中。

結語

Python 並不是最快的編程語言,如果許多特性和改良要得以實現,將需要數年時間。但如果開發者意識到 Python 本身的速度和靈活性之間的權衡,那麽他們現在就可以做很多的事情。

Python 是一種出色的編程語言,可以將業務邏輯的不同部分粘合在一起。而其它語言則非常適合極低級別的優化,通常可以實現快速優化,我們需要找到這些方面的正確平衡點。

0 阅读:3

程序員咋不禿頭

簡介:感謝大家的關注