為什麼 Python 這麼慢?比其他程式語言慢十倍的病根就在「全局解釋器鎖」

Python 程式語言 慢

【為什麼我們要挑選這篇文章】Python 程式語言使用人氣超高,然而 Python 的速度完全比不上其他程式語言。阻礙 Python 行高速運算的理由會是外界因素,還是語言內建?(責任編輯:陳伯安)

Python 語言近年來 人氣爆棚 。它廣泛應用於網絡開發運營,數據科學,網絡開發,以及網絡安全問題中。

然而,Python 在速度上完全沒有優勢可言。

在速度上,Java 如何同 C,C++,C# 或者 Python 相比較?答案幾乎完全取決於要運行的應用。在這個問題上,沒有完美的評判標準,然而 The Computer Language Benchmarks Game 是一個不錯的方法。

The Computer Language Benchmarks Game 連結

基於我對 The Computer Language Benchmarks Game 超過十年的觀察,相比於 Java,C#,Go,JavaScript, C++ 等,Python 是最慢的語言之一。其中包括了 JIT(C#, Java)和 AOT(C, C++)編譯器,以及解釋型語言,例如 JavaScript。

動態編譯解釋

靜態編譯解釋

注意:當我提到「Python」時,我指的是 CPython 這個官方的解釋器。我也將在本文中提及其他的解釋器。

Python 慢其他語言 2 到 10 倍的主要理由:因為他是全局解釋器鎖

我想要回答這樣一個問題:當運行同一個程序時,為什麼 Python 會比其他語言慢 2 到 10 倍?為什麼我們無法將它變得更快?

以下是最主要的原因:

「它是 GIL(Global Interpreter Lock 全局解釋器鎖)」

「它是解釋型語言而非編譯語言」

「它是動態類型語言」

那麼以上哪種原因對性能影響最大呢?

「它是全局解釋器鎖」

現代計算機的 CPU 通常是多核的,並且有些擁有多個處理器。為了充分利用多餘的處理能力,操作系統定義了一種低級的結構叫做線程:一個進程(例如 Chrome 瀏覽器)可以產生多個線程並且指導內部系統。

如果一個進程是 CPU 密集型,那麼其負載可以被多核同時處理,從而有效提高大多數應用的速度。

當我寫這篇文章時,我的 Chrome 瀏覽器同時擁有 44 個線程。注意,基於 POSIX(比如 MacOS 和 Linux)和 Windows 操作系統相比,線程的結構和 API 是不同的。操作系統也會處理線程的調度問題。

如果你之前沒有做過多線程編程,你需要快速熟悉鎖的概念。區別於單線程進程,你需要確保當內存中的變量被修改時,多線程不會同時試圖訪問或者改變同一個存儲地址。

當 CPython 創建變量時,它會預先分配存儲空間,然後計算當前變量的引用數目。這個概念被稱為引用計數。如果引用計數為零,那麼它將從系統中釋放對應存儲區域。

這就是為什麼在 CPython 中創造「臨時」變量不會使應用佔用大量的存儲空間——尤其是當應用中使用了 for 循環這一類可能大量創建「臨時」變量的結構時。

當存在多個線程調用變量時,CPython 如何鎖住引用計數成為了一個挑戰。而「全局解釋鎖」應運而生,它能夠謹慎控制線程的執行。無論有多少的線程,解釋器每次只能執行一個操作。

Python 作為解釋性語言,對性能會造成什麼影響?

這對 Python 的性能意味著什麼呢?

如果你的應用基於單線程、單解釋器,那麼討論速度這一點就毫無意義,因為去掉 GIL 並不會影響代碼性能。

如果你想使用線程在單解釋器(Python 進程)中實現併發,並且你的線程為 IO 密集型(例如網絡 IO 或磁盤 IO),你就會看到 GIL 爭用的結果。

該圖來自 David Beazley 的 GIL 可視化

如果你有一個網絡應用(例如 Django)並且使用 WSGI,那麼每一個對於你的網絡應用的請求將是一個獨立的 Python 解釋器,因此每個請求只有一個鎖。因為 Python 解釋器啓動很慢,一些 WSGI 便集成了能夠使保持 Python 進程的「守護進程」  。

那麼其他 Python 解釋器的速度又如何呢?

PyPy 擁有 GIL,通常比 CPython 快至少三倍。

Jython 沒有 GIL,因為在 Jython 中 Python 線程是用 Java 線程表示的,這得益於 JVM 內存管理系統。

 JavaScript 是如何做到這一點的呢?

首先,所有的 Javascript 引擎使用標記加清除的垃圾收集系統,而之前提到 GIL 的基本訴求是 CPython 的存儲管理算法。

JavaScript 沒有 GIL,但因為它是單線程的,所以也並不需要 GIL。

JavaScript 通過事件循環和承諾/回調模式來實現異步編程的併發。Python 有與異步事件循環相似的過程。

 「因為它是解釋型語言

我經常聽到這句話。我覺得這只是對於 CPython 實際運行方式的一種簡單解釋。如果你在終端中輸入 python myscript.py,那麼 CPython 將對這段代碼開始一系列的讀取,詞法分析,解析,編譯,解釋和運行。

這個過程中的重要步驟是在編譯階段創建一個 .pyc 文件,這個字節碼序列將被寫入 Python3 下 __pycache__/ 路徑中的一個文件(對於 Python2,文件路徑相同)。這個步驟不僅僅應用於腳本文件,也應用於所有導入的代碼,包括第三方模塊。

所以大多時候(除非你寫的代碼只運行一次),Python 是在解釋字節碼並且本地執行。下面我們將 Java 和 C#.NET 相比較:

Java 編譯成一門「中間語言」,然後 Java 虛擬機讀取字節代碼並即時編譯為機器代碼。.NET 的通用中間語言(CIL)是一樣的,它的通用語言運行時間(CLR)也採用即時編譯的方法轉化為機器代碼。

那麼,如果 Python 用的是和 Java 和 C# 一樣的虛擬機和某種字節代碼,為什麼在基準測試中它卻慢得多?首先,.NET 和 Java 是採用 JIT 編譯的。

JIT,又稱即時編譯,需要一種中間語言來把代碼進行分塊(或者叫數據幀)。預編譯(AOT, Ahead of Time)器的設計保證了 CPU 能夠在交互之前理解代碼中的每一行。

JIT 本身不會使執行速度更快,因為它仍然執行相同的字節碼序列。但是,JIT 允許在運行時進行優化。好的 JIT 優化器可以檢測哪些部分執行次數比較多,這些部分被稱為「熱點」。然後,它將用更高效的代碼替換它們,完成優化。

這就意味著當計算機應用程序需要重復做一件事情的時候,它就會更加地快。另外,我們要知道 Java 和 C# 是強類型語言(變量需要預定義),因此優化器可以對代碼做更多的假設。

PyPy 使用即時編譯器,並且前文也有提到它比 CPython 更快。這篇關於基準測試的文章介紹得更為詳細——什麼版本的 Python 最快?

連結:https://hackernoon.com/which-is-the-fastest-version-of-python-2ae7c61a6b2b

為什麼 CPython 不使用即時編譯器呢?

JIT 存在一些缺點:其中一個是啓動時間。CPython 啓動時間已經相對較慢,PyPy 比 CPython 還要慢 2-3 倍。眾所周知,Java 虛擬機的啓動速度很慢。為瞭解決這個問題,.NET CLR 在系統啓動的時候就開始運行,但 CLR 的開發人員還開發了專門運行 CLR 的操作系統來加快它。

如果你有一個運行時間很長的 Python 進程,並且其代碼可以被優化(因為它包含前文所述的「熱點」),那麼 JIT 就能夠起到很大作用。

但是,CPython 適用於各類應用。因此,如果你使用 Python 開發命令行應用程序,每次調用 CLI 時都必須等待 JIT 啓動,這將非常緩慢。

CPython 必須盡量多地嘗試不同的案例以保證通用性,而把 JIT 插入到 CPython 中可能會讓這個項目停滯不前。

如果你想要借助 JIT 的力量,而且你的工作量還比較大,那麼使用 PyPy 吧。

「因為它是一個動態類型語言」

在靜態類型語言中,定義變量時必須聲明類型。C, C++, Java, C#, Go 都是這種語言。

在動態類型語言中,類型的概念依舊存在,但是這個變量的類型是動態變化的。

a = 1

a = “foo”

在上面這個例子中,Python 創建第二個變量的時候用了同樣的名字,但是變量類型是 str(字符型),這樣就對先前在內存中給 a 分配的空間進行了釋放和再分配。

靜態類型語言的這種設計並不是為了麻煩大家——它們是按照 CPU 的運行方式設計的。如果最終需要將所有內容都轉化為簡單的二進制操作,那就必須將對象和類型轉換為低級數據結構。

Python 自動完成了這個過程,我們看不見,也沒必要看見。

不必聲明類型不是使 Python 變慢的原因。Python 語言的設計使我們幾乎可以創建任何動態變量。我們可以在運行時替換對象中的方法,也可以胡亂地把低級系統調用賦給一個值。幾乎怎麼修改都可以。

正是這種設計使得優化 Python 變得異常困難。

為了闡明我的觀點,我將使用一個 MacOS 中的應用。它是一個名為 Dtrace 的系統調用跟蹤工具。CPython 發行版沒有內置 DTrace,因此你必須重新編譯 CPython。以下演示中使用 3.6.6 版本。

wget https://github.com/python/cpython/archive/v3.6.6.zip

unzip v3.6.6.zip

cd v3.6.6

./configure –with-dtrace

make

現在 python.exe 將在整條代碼中使用 Dtrace 跟蹤器。Paul Ross 就 Dtrace 做了一篇很棒的短演講。 你可以下載 Python 的 DTrace 啓動文件來測試函數調用、執行時間、 CPU 時間、系統調用等各種有意思的事情。例如:

sudo dtrace -s toolkit/<tracer>.d -c ‘../cpython/python.exe script.py’

DTrace 啓動文件

演講連結

py_callflow 跟蹤器顯示應用程序中的所有函數調用

Python 的動態類型讓它變慢的嗎?

比較和轉換類型是耗時的,因為每次讀取、寫入變量或引用變量類型時都會進行檢查

很難優化一種如此動態的語言。其他語言之所以那麼快是因為他們犧牲了一定的靈活性,從而提高了性能。

瞭解一下 Cython,它結合了 C-Static 類型和 Python 來優化已知類型的代碼,可以提供 84 倍速度的性能提升。

Python 的緩慢主要是由於它動態和多用途的特點。它可以用於解決幾乎所有問題,但是更加優化而快捷的替代方案可能存在。

但是,有一些方法可以通過利用異步計算,理解分析工具,以及考慮使用多個解釋器來優化 Python 應用程序。

對於有些啓動時間相對不重要,並且即時編譯器(JIT)可以提高效率的應用,可以考慮使用 PyPy。

對於性能優先並且有更多靜態變量的代碼部分,請考慮使用 Cython。

(本文經合作夥伴 大數據文摘 授權轉載,並同意 TechOrange 編寫導讀與修訂標題,原文標題為 〈为什么 Python 这么慢? 〉)

延伸閱讀

《經濟學人》專文探討:「為什麼 Python 是世上最屌的程式語言?」
給工程師的投資入門手冊:Python、R 哪個才是你最適合用來理財的程式語言?
美國工程師花一週寫 Python,用一支機械手臂毀了「威利在哪裡?」這個遊戲


我們正在找夥伴!

2019 年我們的團隊正在大舉擴張,需要你的加入跟我們一起找出台灣創新原動力! 我們正在徵 《採訪社群編輯》、《助理編輯》,詳細職缺與應徵辦法 請點我

點關鍵字看更多相關文章: