1. 程式人生 > >為什麼Python相較於其它主流語言執行要慢?

為什麼Python相較於其它主流語言執行要慢?

Python現在越來越火,已經迅速擴張到了包括DevOps、資料科學、Web開發、資訊保安等各個領域當中。

然而,相比起Python的擴張速度,Python程式碼的執行速度就顯得有點遜色了。

在程式碼執行速度方面,Java、C、C++、C#和Python要如何進行比較呢?並沒有一個放之四海而皆準的標準,因為具體結果很大程度上取決於執行的程式型別,而語言基準測試Computer Language Benchmarks Games可以作為衡量的一個方面。

根據這些年語言基準測試的經驗結果來看,Python比很多語言執行起來都要慢。無論是使用JIT編譯器的C#、Java,還是使用AOT編譯器的C、C++,又或者是JavaScript這些解釋型語言,Python都比它們執行的慢。

注意:對於文中的“Python”,一般指的是CPython這個官方的實現。當然我也會在本文中提到其它語言的Python實現。

我要回答的是這個問題:對於一個類似的程式,Python要比其它語言慢2到10倍不等,這其中的原因是什麼?又有沒有什麼改善的方法呢?

現在主流的說法有這些:

  • 是全域性解釋其鎖Global Interpreter Lock(GIL)的原因
  • 是因為Python是解釋型語言而不是編譯型語言
  • 是因為Python是一種動態型別的語言

那麼到底哪一個才是影響Python執行效率的原因呢?

是全域性直譯器所的原因嗎?

現在很多計算機都配備了具有多個核的CPU,有時甚至還會有多個處理器。 為了更充分的利用它們的處理能力,作業系統定義了一個稱為執行緒的低階結構。 某一個程序(例如Chrome瀏覽器)可以建立多個執行緒,在系統內執行不同的操作。 在這種情況下,CPU密集型程序就可以跨核心分擔負載了,這樣的做法可以大大提高應用程式的執行效率。

例如在寫這篇文章時,我的Chrome瀏覽器打開了44個執行緒。 需要提及的是,基於POSIX的作業系統(例如Mac OS、Linux)和Windows作業系統的執行緒結構、API都是不同的,因此作業系統還負責對各個執行緒的排程

如果你還沒有寫過多執行緒執行的程式碼,你就需要了解一下執行緒鎖的概念了。 多執行緒程序比單執行緒程序更為複雜,是因為需要使用執行緒鎖來確保同一個記憶體地址中的資料不會被多個執行緒同時訪問或更改

CPython直譯器在建立變數時,首先會分配記憶體,然後對該變數的引用進行計數,這稱為引用計數(reference counting)。 如果變數的引用數變為0,這個變數就會從記憶體中被釋放掉

。這就是在for迴圈程式碼塊內建立臨時變數不會增加記憶體消耗的原因。

而當多個執行緒共享一個變數時,CPython鎖定引用計數的關鍵就在於使用了GIL,它會謹慎的控制執行緒的執行情況,無論同時存在多少個執行緒,直譯器每次只允許一個執行緒進行操作。

這會對Python程式的效能產生什麼影響?

如果你的程式只有單執行緒、單程序,程式碼的速度和效能不會受到全域性直譯器鎖的影響。

但如果你通過在單程序中使用多執行緒實現併發,並且是IO密集型(例如網路IO或磁碟IO)的執行緒,GIL競爭的效果就很明顯了。 由 David Beazley 提供的 GIL 競爭情況圖

對於一個Web應用(例如Django),同時還使用了WSGI,那麼對這個Web應用的每一個請求都執行一個單獨的Python直譯器,而且每個請求只有一個鎖。同時因為Python直譯器的啟動比較慢,某些WSGI實現還具有“守護程序模式”,可以使Python程序一直就緒。

其它的Python直譯器表現如何?

PyPy也是一種帶有GIL的直譯器,但通常比CPython要快3倍以上。

Jython則是一種沒有GIL的直譯器,這是因為Jython中的Python執行緒使用的是Java執行緒來實現的,並且由JVM記憶體管理系統來進行管理。

JavaScript在這方面又是怎樣做的呢?

所有的JavaScript引擎使用的都是mark-and-sweep垃圾收集演算法,而GIL使用的則是CPython的記憶體管理演算法。

JavaScript沒有GIL,而且它是單執行緒的,也不需要用到GIL,JavaScript的時間迴圈和Promise/Callback模式實現了以非同步程式設計的方式代替併發。在Python當中也有一個類似的asyncio事件迴圈。

是因為Python是解釋型語言嗎?

我經常會聽到這個說法,但是這過於粗陋的簡化了Python所實際做的工作了。 其實當終端上執行 python myscript.py 之後,CPython會對程式碼進行一系列的讀取、語法分析、解析、編譯、解釋和執行的操作。

而且.pyc檔案的建立是這個過程的重點。 在程式碼編譯階段,Python3會將位元組碼序列寫入__pycache__/下的檔案中,而Python2則會將位元組碼序列寫入當前目錄的.pyc檔案中。 對於你編寫的指令碼、匯入的所有程式碼以及第三方模組都是如此。

因此,絕大多數情況下(除非你的程式碼是一次性的…),Python都會解釋位元組碼並本地執行。 與Java、C#、.NET相比: Java程式碼會被編譯為“中間語言”,有Java虛擬機器讀取位元組碼,並將其即時編譯為機器碼。.NET的CIL(Common-Language-Runtime)也是如此,它將位元組碼即時編譯為機器碼。

既然Python像Java和C#那樣都是用虛擬機器或某種位元組碼,為什麼Python在基準測試中仍然比Java和C#慢得多呢? 首要原因是,.NET和Java都是JIT編譯的。

即時編譯Just-In-Time(JIT)需要一種中間語言,以便將程式碼拆分為多個塊(或多個幀)。而提前編譯Ahead-Of-Time(AOT)則需要確保CPU在任何交互發生之前理解每一行程式碼。

JIT本身不會使執行速度加快,因為它執行的仍然是同樣的位元組碼序列。但是JIT會允許在執行時進行優化。 一個優秀的JIT優化器會分析出程式的那些部分會被多次執行,這就是程式中的“熱點”,然後優化器會將這些程式碼替換為更有效率的版本以實現優化。

這就意味著如果你的程式是多次重複相同的操作時,有可能會被優化器優化的更快。而且,Java和C#是強型別語言,因此優化器對程式碼的判斷可以更為準確。

PyPy使用了明顯快於CPython的JIT。

那為什麼CPython不使用JIT呢?

JIT也不是完美的,它的一個顯著的缺點就在於啟動時間。 CPython的啟動時間已經相對比較慢了,而PyPy比CPython啟動還要慢2到3倍。 Java虛擬機器啟動速度也是出了名的慢。.NET的CLR則通過在系統啟動時啟動來優化體驗,而CLR的開發者也是在CLR上開發該作業系統。

因此如果你有個長時間執行的單一Python程序,JIT就比較有意義了,因為程式碼裡有“熱點”可以優化。 不過,CPython是個通用的實現。設想如果使用Python開發命令列程式,但每次呼叫CLI時都必須等待JIT緩慢啟動,這種體驗就相當不好了。

CPython試圖用於各種使用情況。有可能實現將JIT插入到CPython中,但這個改進工作的進度基本處於停滯不前的狀態。 如果你想充分發揮JIT的優勢,請使用PyPy。

是因為Python是一種動態型別的語言嗎?

在C、C++、Java、C#、Go這些靜態型別語言中,必須在宣告變數時指定變數的型別。而在動態型別語言中,雖然也有類似的概念,但變數的型別是可以改變的。 例如:

a = 1  
a = 'foo'  

在上面這個例項裡,Python將變數a一開始儲存整數型別變數的記憶體空間釋放了,並建立了一個新的儲存字串型別的記憶體空間,並且和原來的變數同名。

靜態型別語言這樣的設計並不是為難你,而是為了方便CPU執行而這樣設計的。 因為最終都需要將所有操作都對應為簡單的二進位制操作,因此必須將物件、型別這些高階的資料結構轉換為低階資料結構。

Python也實現了這樣的轉換,但使用者看不到這些轉換,也不需要關心這些轉換。

不用必須宣告型別並不是為了使Python執行慢,Python的設計是讓使用者可以讓各種東西變得動態:可以在執行時更改物件上的方法,也可以在執行時動態新增底層系統呼叫到值的宣告上,幾乎可以做到任何事。

但也正是這種設計使得Python的優化異常的難。

為了證明我的觀點,我使用了一個Mac OS上的系統呼叫跟蹤工具DTrace。CPython釋出版本中沒有內建DTrace,因此必須重新對CPython進行編譯。下面以Python3.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'  

py_callflow追蹤器顯示了程式裡呼叫的所有函式。 那麼,Python的動態型別會讓它變慢嗎?

  • 型別比較和型別轉換消耗的資源時比較多的,每次讀取、寫入或引用變數時都會檢查變數的型別
  • Python的動態程度讓它難以被優化,因此很多Python的替代品能夠如此快都是為了提升速度而在靈活性上作出了妥協
  • 而CPython結合了C的靜態型別和Python來優化已知型別的程式碼,它可以將效能提升84倍

總結

由於Python是一種動態、多功能的語言,因此執行起來會相對緩慢。對於不同的實際需求,可以使用各種不同的優化或替代方案。

例如可以使用非同步程式設計,引入分析工具或使用多種直譯器來優化Python程式。 對於不要求啟動時間且程式碼可以充分利用JIT的程式,可以考慮使用PyPy。 而對於看重效能並且靜態型別變數較多的程式,不妨使用Cython。

本文轉自: