Python語言近年來人氣爆棚。它廣泛應用於網路開發運營,資料科學,網路開發,以及網路安全問題中。
然而,Python在速度上完全沒有優勢可言。
在速度上,Java如何同C,C++,C#或者Python相比較?答案几乎完全取決於要執行的應用。在這個問題上,沒有完美的評判標準,然而The Computer Language Benchmarks Game 是一個不錯的方法。
連結:
http://benchmarksgame.alioth.debian.org
基於我對The Computer Language Benchmarks Game超過十年的觀察,相比於Java,C#,Go,JavaScript, C++等,Python是最慢的語言之一。其中包括了 JIT (C#, Java) 和 AOT (C, C++)編譯器,以及解釋型語言,例如JavaScript。
動態編譯:
https://en.wikipedia.org/wiki/Just-in-time_compilation
靜態編譯:
https://en.wikipedia.org/wiki/Ahead-of-time_compilation
注意:當我提到“Python”時,我指的是CPython這個官方的直譯器。我也將在本文中提及其他的直譯器。
我想要回答這樣一個問題:當執行同一個程式時,為什麼Python會 比其他語言慢2到10倍?為什麼我們無法將它變得更快?
以下是最主要的原因:
“它是GIL(Global Interpreter Lock全域性直譯器鎖)”
“它是解釋型語言而非編譯語言”
“它是動態型別語言”
那麼以上哪種原因對效能影響最大呢?
“它是全域性直譯器鎖”
現代計算機的CPU通常是多核的,並且有些擁有多個處理器。為了充分利用多餘的處理能力,作業系統定義了一種低階的結構叫做執行緒:一個程式(例如Chrome瀏覽器)可以產生多個執行緒並且指導內部系統。
如果一個程式是CPU密集型,那麼其負載可以被多核同時處理,從而有效提高大多數應用的速度。
當我寫這篇文章時,我的Chrome瀏覽器同時擁有44個執行緒。注意,基於POSIX(比如MacOS和Linux)和Windows作業系統相比,執行緒的結構和API是不同的。作業系統也會處理執行緒的排程問題。
如果你之前沒有做過多執行緒程式設計,你需要快速熟悉鎖的概念。區別於單執行緒程式,你需要確保當記憶體中的變數被修改時,多執行緒不會同時試圖訪問或者改變同一個儲存地址。
當CPython建立變數時,它會預先分配儲存空間,然後計算當前變數的引用數目。這個概念被稱為引用計數。如果引用計數為零,那麼它將從系統中釋放對應儲存區域。
這就是為什麼在CPython中創造“臨時”變數不會使應用佔用大量的儲存空間——尤其是當應用中使用了for迴圈這一類可能大量建立“臨時”變數的結構時。
當存在多個執行緒呼叫變數時,CPython如何鎖住引用計數成為了一個挑戰。而“全域性解釋鎖”應運而生,它能夠謹慎控制執行緒的執行。無論有多少的執行緒,直譯器每次只能執行一個操作。
這對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啟動檔案:
https://github.com/paulross/dtrace-py/tree/master/toolkit
演講連結:
https://github.com/paulross/dtrace-py#the-lightning-talk
py_callflow跟蹤器顯示應用程式中的所有函式呼叫
因此,是Python的動態型別讓它變慢的嗎?
比較和轉換型別是耗時的,因為每次讀取、寫入變數或引用變數型別時都會進行檢查
很難最佳化一種如此動態的語言。其他語言之所以那麼快是因為他們犧牲了一定的靈活性,從而提高了效能。
瞭解一下Cython,它結合了C-Static型別和Python來最佳化已知型別的程式碼,可以提供84倍速度的效能提升。
結論
Python的緩慢主要是由於它動態和多用途的特點。它可以用於解決幾乎所有問題,但是更加最佳化而快捷的替代方案可能存在。
但是,有一些方法可以透過利用非同步計算,理解分析工具,以及考慮使用多個直譯器來最佳化Python應用程式。
對於有些啟動時間相對不重要,並且即時編譯器(JIT)可以提高效率的應用,可以考慮使用PyPy。
對於效能優先並且有更多靜態變數的程式碼部分,請考慮使用Cython。