尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
點擊藍字「速學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 要比其它語言慢 2 到 10 倍不等,這其中的原因是什麼?又有沒有改善的方法呢?
主流的說法有這些:python是一個動態的解釋型語言;python中的值不是存儲在緩存區而是分散的存儲在對象中。通過使用Numpy和Scipy等相關可以進行矢量化操作的工具並調用編譯後的代碼來繞過這個問題來避開這個問題。
一、動態類型導致運行速度慢
在其他的文章中有較為詳細的解釋,也有舉例說明,本文沒有例子講解只是提取了原理來講解,內容主要如下:
a、動態語言中的執行過程
Python等動態類型語言之所以慢,就是因為每一個簡單的操作都需要大量的指令才能完成。他們的虛擬機擁有很強的優化器,卻是為靜態語言設計的。對Python幾乎沒有效果。
舉一個例子。對於整數加法,C語言很簡單,只要一個機器指令ADD就可以了,最多不過再加一些內存讀寫。但是,對於Python來說,a+b這樣的簡單二元運算,可就真的很麻煩了。Python是動態語言,變量只是對象的引用,變量a和b本身都沒有類型,而它們的值有類型。所以,在相「加」之前,必須先判斷類型。
1. 判斷a是否為整數,否則跳到第9步2. 判斷b是否為整數,否則跳到第9步3. 將a指向的對象中的整數值讀出來4. 將b指向的對象中的整數值讀出來5. 進行整數相加6. 生成一個新整數對象7. 將運算結果存進去8. 返回這個對象,完成!9. 判斷a是否為字符串,否則跳到第13步10. 判斷b是否為字符串,否則跳到第13步11. 進行字符串串接操作,生成一個新字符串對象12. 返回這個對象,完成!13. 從a的字典里取出__add__方法14. 調用這個方法,將a和b作為參數傳入15. 返回上述方法的返回值。這還只是簡化版的,實際中還要考慮溢出問題等。
b、Jpython
Jython能做的只是把Python代碼轉換成JVM的代碼,而Python中那些判斷a,b是否為整數或者字符串的操作是不能省略的。畢竟Python是允許你寫1+2同時也可以”hello”+”world”。這種運行時的類型檢查並不能簡單地通過編譯而去除。
c、Google的Unladen Swallow項目
它是一個很有雄心的項目,但是,在項目開始一年後就流產了。最後,加速效果也不過50%左右。它們使用的方法是樸素的「模板編譯法」:看到Python的加法操作,就轉換成一個C語言的函數調用,調用Python的PyNumber_Add函數。這個函數就是幹類似上面一串的事。同樣地,雖然去除了官方Python的解釋器代價,但並沒有消除運行時類型檢查的代價。
d、pypy為什麼比較快和pypy的不足:
PyPy可以將Python的速度加到C的一半左右。PyPy使用了一種技巧,就是「類型推導」(Type Inference)。PyPy的運行時編譯器(Just-in-time compiler,或者稱JIT Compiler)的工作方式是,只優化循環,因為大量的時間都是消耗在少數循環上。當運行時檢測到某個循環運行的次數很多的時候,就開啟一個「錄像機」,錄制這個循環執行一次中,執行的所有操作的軌跡。這樣以「軌跡」為單位的編譯方式叫Tracing JIT。當類型確定以後,其中涉及的數據都是整數,都可以直接對應機器指令進行執行。程序起碼在這一部分已經由動態的代碼變成像C一樣的靜態類型代碼了,而且數據類型很接近機器。將這一段代碼編譯成機器碼,效率就可以和C相比了。
注意到,這其實是一種「猜測」:優化器「猜想」每次執行循環for i in range(n),i和n都是整數。這種猜測是可能出錯的。萬一工程師將一個字符串傳入函數怎麼辦呢?所以,基於「猜測」(speculation)的優化必須考慮「猜錯了」的情形。這就是優化過的代碼的第1、3、11行的用途。1和3考慮萬一i和n不是整數的情形,而15考慮了整數溢出的情況。
在Python里,整數都是高精度整數,可以是任意大的,而不僅限於32位。(其實上述32位也只是假設,在64位機上,顯然64位效率更高。)所以,如果猜錯了(這種事經常會發生),就必須停止執行這段「優化」過的代碼,而是老老實實回到解釋器中,像傳統的Python一樣執行。
可以看出,帶有類型推導功能的Tracing JIT編譯器可以大幅度加快動態語言的速度。
主要原因是:
1. 在運行時得到了變量的類型,並通過「猜測」,將這些類型轉換成接近機器的類型。
2. 將簡化的操作編譯成機器碼,去除了解釋器的代價。目前,PyPy是一個很活躍的項目。但是,畢竟是一個研究型的項目,PyPy也有自己的不足。如和官方Python並不完全兼容;PyPy本身的可執行文件很大;並不是運行所有的程序都快——PyPy雖然JIT Compiler很快,但它的解釋器速度不如官方的Python,對於無法通過優化加速的程序來說,PyPy就不快了。
二、python慢不僅是因為動態類型
甚至不是python慢的主要原因,在現實中,在C語言和Python在運行時的巨大的不同是由於數據結構和算法的不同。
用Python寫不同的代碼
讓我們用一個實例來說明問題。
一個Python工程師可能很喜歡用下面的例子表示一個平面上的點:
point = {'x': 0, 'y': 0}
這種方法很易讀,容易編碼,形式很優雅。
另一個方面,一個C語言工程師可能使用結構體來表示平面上的點:
struct Point { int x; int y;};
盡管這種方法也和Python能一樣的工作並且都是很優雅的,但這是完全不同的數據結構。這里我們告訴了編譯器,我們有兩個字段x和y。知道了這兩個字段的類型,編譯器將分配一塊連續的內存來儲存這兩個數據。換一句話說,就像一個數組一樣。任何時間,編譯器都知道給定的x和y在哪里。我們可以很容易地訪問這些數據,就像是訪問某些常數據一樣。
Python使用哈希散列的方法來解決類似的問題。所以編譯器不能簡單地分配連續內存存儲x和y來處理這些問題。由於我們在其中任意的地方都可能出現這些鍵。
如果我們想的話,我們也可能刪除這些鍵。編譯器必須要使用哈希函數來映射到你可能讓他指向的任何存儲單元。不用說,這些函數增加了處理時間。盡管也許減緩的很小,但是足可以拖慢你的代碼,尤其是這種情況如果很多的時候。
如果就是想將Python翻譯成C語言的話,可能就像下面這樣:
std::hash_set point;point[「x」] = xpoint[「y」] = y
看這個代碼片段,好像就是語言的設計者他們自己故意盡力使哈希表複雜,因此盡管是正確的,但沒有人使用。由於這個原因,寫C語言的人可能認為這是不可思議的,但為什麼在Python就是可以接受的呢?
原因就是寫Python代碼的人的「dictionaries are lightweight objects」這種心態。
看下面的代碼,這在Python中最接近C語言結構體:
class Point(object): x, y = None, None def __init__(self, x, y): self.x, self.y = x, y
這對編譯器是有用的,就像是C語言的結構體。例如第二行,我們明確告訴編譯器但我們創造一個對象時我們總是至少需要兩個數據段,我們希望編譯器處理這個問題。
不幸的是這種標準的Python被叫做CPthon,不能總被使用。
在我的機器上,下面的代碼要執行186毫秒:
def sum_(points): sum_x, sum_y = 0, 0 for point in points: sum_x += point['x'] sum_y += point['y'] return sum_x, sum_y
在我的機器上,用point.x代替point[‘x’]會花費201毫秒。也就是說,會慢了8%。
在CPthon中,point.x通常就是被處理成dict(point)[‘x’]。這意味著帶著點的class仍然像以前一樣使用字典(dictionary)的方法查找。這樣的話,就很容易看出為什麼directionary的方法被看為「輕量級的」。
一些Python寫的代碼就是為了效率而設計的,例如PyPy,能很快地執行。如果不使用Python而是使用PyPy,同樣的代碼片段執行時間分別是21.6和3.75毫秒。這種方法相比CPython在JIT-capable編譯情況下結果都是令人滿意的。換一句話說,PyPy能正確地使用數據結構。
我希望你再一次看這個最短時間3.75毫秒。這個數字表明我們能在一秒進行266000次運算,這些事來自Python的,其中有動態綁定,monkey-patching(在不改變源代碼的情況下擴展或修改動態語言運行時代碼的方法)等。
所有的這些,都是在編碼和做到中使用了更好的數據結構。下一次當你在用Python寫一行代碼時,想一想你在使用什麼數據結構,顯示的還是隱式的,考慮一下是否有更好的辦法。這就是你用C語言寫程序時考慮的,不是嗎?
總結
由於 Python 是一種動態、多功能的語言,因此運行起來會相對緩慢。對於不同的實際需求,可以使用各種不同的優化或替代方案。
例如可以使用異步,引入分析工具或使用多種解釋器來優化 Python 程序。
對於不要求啟動時間且代碼可以充分利用 JIT 的程序,可以考慮使用 PyPy。
而對於看重性能並且靜態類型變量較多的程序,不妨使用Cython。
最後,我願意相信這個文章是表明為什麼Python是一個有前途的語言的一個清楚的例子(或者是類似的語言)。這表明了標準的Python做到,這里的CPython僅僅是作為一個參考,它從來就不是被設計用來更快地執行的。正如我們今天可以看到的,像PyPy一樣的算法做到是可以優化你的代碼到一個很好的長度。隨著語言的自然發展,這些優化是可能的。我們僅僅用Python編程過23年,那麼如果像C語言一樣有42年的發展,Python會是什麼樣子呢?
今天就聊到這里啦,大家記得點讚收藏,分享轉PO,關注小姐姐喲!
END
更多python相關資料
直接掃描下方的微信二維碼
來撩我們炒雞可愛的無雙老師