同為程序員的心頭好,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 這種解釋語言。
注:本文中所說的“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 競爭:
來自於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跟蹤器會顯示應用程序的所有函數調用。
- 那麼,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基礎到高級適合不同學習階段,希望大家學習愉快。獲取方式:點擊小編頭像,關注後私信回覆“資料”即可下載。
閱讀更多 Python技術 的文章