熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?

熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?

同為程序員的心頭好,Python 為什麼能這麼慢?

如果你是Python初學者,文末提供Python資料可下載看看.

眼下 Python 異常火爆,不論是 DevOps、數據科學、Web 開發還是安全領域,都在用 Python——但是它在速度上卻沒有任何優勢。

與 C、C++、C# 或 Python 相比,Java 的速度如何?答案很大程度上依賴於你需要運行的應用種類。世上沒有完美的性能測試,但計算機語言評測遊戲(Computer Language Benchmarks Game)是個很好的測試方式:http://algs4.cs.princeton.edu/faq/。

我從十年前就開始談論計算機語言評測遊戲。與 Java、C#、Go、JavaScript、C++ 等其他語言相比,Python 是最慢的語言之一。這裡包括JIT(Just In Time)語言(如C#、Java)和 AOT(Ahead Of Time)語言(C、C++)編譯器,也有 JavaScript 這種解釋語言。

熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?

注:本文中所說的“Python”是指語言的具體實現,即 CPython。本文也會提到其他運行。

我希望回答以下問題:如果 Python 完成相同的任務要花費其他語言二至十倍的時間,那麼它為什麼慢,能不能更快一些呢?

以下是幾種常見的原因:

  • “因為它是GIL(全局解釋器鎖)”
  • “因為它是解釋語言不是編譯語言”
  • “因為它是動態類型語言”

究竟哪個原因對性能的影響最大?

1、“因為它是GIL”

現代計算機的 CPU 有多個核心,有時甚至有多個處理器。為了利用所有計算能力,操作系統定義了一個底層結構,叫做線程,而一個進程(例如 Chrome瀏覽器)能夠生成多個線程,通過線程來執行系統指令。這樣如果一個進程是要使用很多 CPU,那麼計算負載就會由多個核心分擔,最終使得絕大多數應用能更快地完成任務。

在撰寫本文時,我的 Chrome 瀏覽器開了 44 個線程。另外,基於 POSIX 的操作系統(如 Mac OS 和 Linux)的線程結構和 API 與 Windows 操作系統是不一樣的。操作系統還負責線程的調度。

如果你沒寫過多線程程序,那麼你應該瞭解一下鎖的概念。與單線程進程不同,在多線程編程中,你要確保改變內存中的變量時,多個線程不會試圖同時修改或訪問同一個內存地址。

CPython 在創建變量時會分配內存,然後用一個計數器計算對該變量的引用的次數。這個概念叫做“引用計數”。如果引用的數目為 0,那就可以將這個變量從系統中釋放掉。這樣,創建“臨時”變量(如在 for 循環的上下文環境中)不會耗光應用程序的內存。

隨之而來的問題就是,如果變量在多個線程中共享,CPython 需要對引用計數器加鎖。有一個“全局解釋器鎖”會謹慎地控制線程的執行。不管有多少個線程,解釋器一次只能執行一個操作。

這對 Python 應用的性能有什麼影響?

如果應用程序是單線程、單解釋器的,

那麼這不會對速度有任何影響。去掉 GIL 也不會影響代碼的性能。

但如果想用一個解釋器(一個 Python 進程)通過線程實現併發,而且線程是IO 密集型的(即有很多網絡輸入輸出或磁盤輸入輸出),那麼就會出現下面這種 GIL 競爭:

熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?

來自於David Beazley的“圖解GIL”一文:http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html

如果 Web 應用(如 Django)使用了 WSGI,那麼發往 Web 應用的每個請求都會由獨立的 Python 解釋器執行,因此每個請求都只會有一個鎖。由於 Python 解釋器啟動很慢,一些 WSGI 實現就支持“守護模式”,保持 Python 進程長期運行。

其他 Python 運行時如何?

PyPy 的 GIL 通常要比 CPython 快三倍以上。

Jython 沒有 GIL 因為 Jython 中的 Python 線程由 Java 線程表示,因此能享受到 JVM 內存管理系統的好處。

JavaScript 怎麼處理這個問題i?

首先,所有 JavaScript 引擎都是用標記-清除垃圾回收算法。如前所述,對 GIL 的需求主要是由 CPython 的內存管理算法導致的。

JavaScript 沒有 GIL,但它也是單線程的,所以它根本不需要。JavaScript 的時間循環和 Promise/Callback 模式實現了異步編程,取代了併發編程。Python 也能通過 asyncio 的事件循環實現類似的模式。

2、“因為它是解釋語言”

這條理由我也聽過很多,我發現它過於簡化了 CPython 的實際工作原理。當你在終端上寫 python myscript.py 時,CPython 會啟動一長串操作,包括讀取、詞法分析、語法分析、編譯、解釋以及執行。

如果你對這些過程感興趣,可以看看我之前寫的文章:

6分鐘修改Python語言:https://hackernoon.com/modifying-the-python-language-in-7-minutes-b94b0a99ce14

這個過程的重點就是它會在編譯階段生成.pyc文件,字節碼會寫到__pycache__/下的文件中(如果是Python 3),或者寫到與源代碼同一個目錄中(Python 2)。不僅你編寫的腳本是這樣,所有你導入的代碼都是這樣,包括第三方模塊。

因此絕大多數情況下(除非你寫的代碼只會運行一次),Python是在解釋字節碼並在本地執行。與Java和C#.NET比較一下:

Java將源代碼編譯成“中間語言”,然後Java虛擬機讀取字節碼並即時編譯成機器碼。.NET CIL也是一樣的,.NET的公共語言運行時(CLR)使用即時編譯將字節碼編譯成機器碼。

那麼,既然它們都使用虛擬機,以及某種字節碼,為什麼Python在性能測試中比Java和C#慢那麼多?第一個原因是,.NET和Java是即時編譯的(JIT)。

即時編譯,即JIT(Just-in-time),需要一種中間語言,將代碼分割成小塊(或者稱幀)。而提前編譯(Ahead of Time,簡稱AOT)是編譯器把源代碼翻譯成CPU能理解的代碼之後再執行。

JIT本身並不能讓執行更快,因為它執行的是同樣的字節碼序列。但是,JIT可以在運行時做出優化。好的GIT優化器能找到應用程序中執行最多的部分,稱為“熱點”。然後對那些字節碼進行優化,將它們替換成效率更高的代碼。

這就是說,如果你的應用程序會反覆做某件事情,那麼速度就會快很多。此外,別忘了Java和C#都是強類型語言,所以優化器可以對代碼做更多的假設。

前面說過,PyPy有個JIT,因此它比CPython要快很多。下面這篇性能測試的文章介紹得更詳細:

哪個版本的Python最快?

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

那麼為什麼CPython不用JIT?

JIT也有缺點:首先就是啟動速度。CPython的啟動速度已經比較慢了,而PyPy的啟動速度要比CPython慢兩到三倍。Java虛擬機的啟動速度也是出了名的慢。.NET CLR在系統啟動時啟動,因此避免了這個問題,但這要歸功於CLR和操作系統是同一撥開發者開發的。

如果你有一個Python進程需要運行很長時間,而且代碼裡包含“熱點”可以被優化,那麼使用JIT就很不錯。

但是,CPython是個通用的實現。因此如果要用Python開發命令行程序,那麼每次都要等待JIT調用CLI就特別慢了。

CPython試圖滿足大部分情況下的需求。有一個在CPython中實現JIT(https://www.slideshare.net/AnthonyShaw5/pyjion-a-jit-extension-system-for-cpython)的項目,不過這個項目已經停止很久了。

如果你想要享受JIT的好處,並且要處理的任務適合JIT,那就使用PyPy。

3、“因為它是動態類型語言”

“靜態類型”語言要求必須在變量定義時指定其類型,例如C、C++、Java、C#和Go等。

而動態類型語言中儘管也有類型的概念,但變量的類型是動態的。

a = 1

a = "foo"

在這個例子中,Python用相同的名字和str類型定義了第二個變量,同時釋放了第一個a的實例佔用的內存。

靜態類型語言的設計目的並不是折磨人,這樣設計是因為CPU就是這樣工作的。如果任何操作最終都要轉化成簡單的二進制操作,那就需要將對象和類型都轉換成低級數據結構。

Python幫你做了這一切,只不過你從來沒有關心過,也不需要關心。

不需要定義類型並不是Python慢的原因。Python的設計可以讓你把一切都做成動態的。你可以在運行時替換對象的方法,可以在運行時給底層系統調用打補丁。幾乎一切都有可能。

而這種設計使得Python的優化變得很困難。

為了演示這個觀點,我使用了一個Mac OS下的系統調用跟蹤工具,叫做Dtrace。CPython的發佈並不支持DTrace,因此需要重新編譯CPython。演示中用的是Python 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的演講(https://github.com/paulross/dtrace-py#the-lightning-talk)。可以從這裡下載DTrace用於Python的文件(https://github.com/paulross/dtrace-py/tree/master/toolkit)用來測量函數調用、執行時間、CPU時間、系統調用以及各種函數等等。

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

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

熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?

  • 那麼,Python的動態類型是否讓Python更慢?
  • 比較並轉換類型的代價很大。每次讀取、寫入或引用變臉時都會檢查類型
  • 動態類型的語言很難優化。許多替代Python的語言很快的原因就是它們犧牲了便利性來交換性能。
  • 例如Cython(http://cython.org/),它通過結合C的靜態類型和Python的方式,使得代碼中的類型已知,從而優化代碼,能夠獲得84倍的性能提升(http://notes-on-cython.readthedocs.io/en/latest/std_dev.html)

結論

Python慢的主要原因是因為它的動態和多樣性。它能用於解決各種問題,但多數問題都有優化得更好和更快的解決方案。

但Python應用也有許多優化措施,如使用異步、理解性能測試工具,以及使用多解釋器等。

對於啟動時間不重要,而代碼可能享受到JIT的好處的應用,可以考慮使用PyPy。

對於代碼中性能很重要的部分,如果變量大多是靜態類型,可以考慮使用Cython。

Python學習書籍推薦

很多人在問,學習Python讀什麼書,這其實是一個非常通用的問題,學習分為2種方式:看書、上課,而讀書學習是最實惠也是最高效的一種,小編整理了一些Python高分書籍給大家,從0基礎到高級適合不同學習階段,希望大家學習愉快。獲取方式:點擊小編頭像,關注後私信回覆“資料”即可下載。

熱度碾壓 Java、C 、C++的 Python,為什麼速度那麼慢?


分享到:


相關文章: