首頁 > 軟體

pypy真的能讓python比c還快?

2021-05-18 13:30:18

「來源: |Python爬蟲與資料探勘 ID:crawler_python」

回覆「書籍」即可獲贈Python從入門到進階共10本電子書

仰天大笑出門去,我輩豈是蓬蒿人。

最近 「pypy為什麼能讓python比c還快」 刷屏了,原文講的內容偏理論,乾貨比較少。我們可以再深入一點點,瞭解pypy的真相。

正式開始之前,多嘮叨兩句。我司發力多個賽道的遊戲,其中包括某魚類遊戲Top2項目,拿過阿拉丁神燈獎的SLG卡牌小遊戲項目和海外三消遊戲。這些不同類型的遊戲,後端大多是使用的是pypy。對於如何使用pypy,我有一點使用經驗可以聊聊。話不多說,正式開始,本文包括下面幾個部分:

語言分類python的直譯器實現pypy為什麼快效能比較效能優化方法pypy的特性小結語言分類

我們先從最基本的一些語言分類概念聊起,對這部分內容非常瞭解的朋友可以跳過。

靜態語言 vs 動態語言

如果在編譯時知道變數的類型,則該語言為靜態類型。靜態類型語言的常見示例包括Java,C,C ++,FORTRAN,Pascal和Scala。在靜態類型語言中,一旦使用類型聲明瞭變數,就無法將其分配給其他不同類型的變數,這樣做會在編譯時引發類型錯誤。

# javaint data;data = 50;data = 「Hello Game_404!」; // causes an compilation error如果在運行時檢查變數的類型,則語言是動態類型的。動態類型語言的常見示例包括JavaScript,Objective-C,PHP,Python,Ruby,Lisp和Tcl。在動態類型語言中,變數在運行時通過賦值語句繫結到物件,並且可以在程式執行期間將相同的變數繫結到不同類型的物件。

# pythondata = 10;data = "Hello Game_404!"; // no error causeddata = data + str(10)一般來說靜態語言編譯成位元組碼執行,動態語言使用直譯器執行。編譯型語言效能更高,但是較難移植到不同的CPU架構體系和作業系統。解釋型語言易於移植,效能會比編譯語言要差得多。這是頻譜的兩個極端。

強類型語言 vs 弱類型語言

強類型語言是一種變數被繫結到特定資料類型的語言,如果類型與表示式中的預期不一致,將導致類型錯誤,比如下面這個:

# pythontemp = 「Hello Game_404!」temp = temp + 10; // program terminates with below stated error (TypeError: must be str, not int)python和我們感覺不一致,背叛了弱類型語言,不像世界上最好的語言:(

# php$temp = 「Hello Game_404!」;$temp = $temp + 10; // no error causedecho$temp;常見程式語言的象限分類如下圖:

language這一部分內容主要翻譯自參考連結1

python的直譯器實現

python是一門動態程式語言,由特定的直譯器解釋執行。下面是一些直譯器實現:

CPython 使用c語言實現的直譯器PyPy 使用python語言的子集RPython實現的直譯器,一般情況下PyPy比CPython快4.2倍Stackless Python 帶有協程實現的直譯器Jython Java實現的直譯器IronPython .net實現的直譯器Pyston 一個較新的實現,是CPython 3.8.8的一個分支,具有其他針對性能的優化。它針對大型現實應用程式(例如Web服務),無需進行開發工作即可提供高達30%的加速。...還有幾個相關概念:

IPython && Jupyter ipython是使用python構建的互動式shell, Jupyter是其web化的包裝。Anaconda 是一個python虛擬環境,Python資料科學常用。mypyc 一個新的項目,將python編譯成c程式碼庫,以期提高python的運行效率。py檔案和pyc檔案 pyc檔案是python編譯後的位元組碼,也可以由python直譯器執行。wheel檔案和egg檔案 都是項目版本釋出的打包檔案,wheel是最新標準。...這裡大家會有一個疑問,python不是解釋型語言嘛?怎麼又有編譯後的pyc。是這樣的: py檔案編譯成pyc後,直譯器預設 優先 執行pyc檔案,這樣可以加快python程式的 啟動速度 (注意是啟動速度)。繼背叛弱類型語言後,python這個鬼又在編譯語言和解釋語言之間橫跳。

還有一個事件是Go語言在1.5版本實現自舉。Go語言在1.5版本之前使用c實現的編譯器,在1.5版本時候使用Go實現了自己的編譯器,這裡有一個雞生蛋和蛋生雞的過程,也挺有意思。

pypy為什麼快

pypy使用python的子集rpython實現瞭解釋器,和前面介紹的Go的自舉有點類似。反常識的是rpython的直譯器會比c實現的直譯器快?主要是因為pypy使用了JIT技術。

Just-In-Time (JIT) Compiler 試圖通過對機器碼進行一些實際的編譯和一些解釋來獲得兩全其美的方法。簡而言之,以下是JIT編譯為提高效能而採取的步驟:

標識程式碼中最常用的元件,例如迴圈中的函數。在運行時將這些零件轉換為機器碼。優化生成的機器碼。用優化的機器碼版本交換以前的實現。這也是 「pypy為什麼能讓python比c還快」 一文中的示例展現出來的能力。pypy除了速度快外,還有下面一些特點:

記憶體使用情況比cpython少gc策略更優化Stackless 協程模式預設支援,支援高併發相容性好,高度相容cpython實現,基本可以無縫切換以上都是宣稱

pypy這麼強,快和省都佔了,為什麼沒有大規模流行起來呢? 我個人認為,主要還是python的原因。

python生態中大量庫採用c實現,特別是科學計算/AI相關的庫,pypy在這塊並沒有優勢。pypy快的主要在pure-python,也就是純粹的python實現部分。pypy適合長駐記憶體的高併發應用(web服務類)python是一門膠水語言,並不追求效能極致,即使快4倍也不夠快:( 。肯定比不上c,原文中的c應該是 偷換了概念 ,指c實現的cpython直譯器。需要注意的是,pypy一樣也有GIL的存在, 所以高併發主要在stackless。

這一部分內容參考自參考連結2

效能比較

我們可以編寫效能測試用例,用程式碼說話,對各個實現進行對比。本文的測試用例並不嚴謹,不過也足夠說明一些問題了。

開車和步行

原文中累加測試用例是100000000次,我們減少成1000次:

import timestart = time.time()number = 0for i in range(1000): number += iprint(number)print(f"Elapsed time: {time.time() - start} s")測試結果如下表(測試環境在本文附錄部分):

結果顯示運行1000次迴圈的情況下cpython要比pypy快,這和迴圈100000000次 相反。用下面的例子可以非常形象的解釋這一點。

假設您想去一家離您家很近的商店。您可以步行或開車。您的汽車顯然比腳快得多。但是,請考慮需要執行以下操作:

去你的車庫。啟動你的車。讓汽車暖一點。開車去商店。查詢停車位。在返回途中重複該過程。開車要涉及很多開銷,如果您想去的地方在附近,這並不總是值得的!現在想想如果您想去五十英里外的鄰近城市會發生什麼。開車去那裡而不是步行肯定是值得的。

舉例來自參考連結2

儘管速度的差異並不像上面類比那麼明顯,但是PyPy和CPython的情況也是如此。

橫向對比

我們橫向對比一下c,python3, pypy3, js 和lua的效能。

# jsconst start = Date.now();let number = 0for (i=0;i<100000000;i++){ number += i}console.log(number)const millis = Date.now() - start;console.log(`milliseconds elapsed = `, millis);# lualocal starttime = os.clock();local number = 0local total = 100000000-1for i=total,1,-1 do number = number+iendprint(number)local endtime = os.clock();print(string.format("elapsed time : %.4f", endtime - starttime));# c#include <stdio.h>#include <time.h>const long long TOTAL = 100000000;long long mySum(){ long long number=0; long long i;for( i = 0; i < TOTAL; i++ ) { number += i; }return number;}int main(void){ // Start measuring time clock_t start = clock();printf("%llu n", mySum()); // Stop measuring time and calculate the elapsed time clock_t end = clock(); double elapsed = (end - start)/CLOCKS_PER_SEC;printf("Time measured: %.3f seconds.n", elapsed);return 0;}

測試結果可見,c無疑是最快的,秒殺其它語言,這是編譯語言的特點。在解釋語言中,pypy3表現配得上優秀二字。

記憶體佔用

測試用例中增加記憶體佔用的輸出:

p = psutil.Process()mem = p.memory_info()print(mem)測試結果如下:

# python3pmem(rss= 9027584, vms=4747534336, pfaults= 2914, pageins=1)# pypy3pmem(rss=39518208, vms=5127745536, pfaults=12188, pageins=58)pypy3的記憶體佔用會比python3要高,這個才科學,用記憶體空間換了運行時間。當然這個評測並不嚴謹,實際情況如何,pypy宣稱的記憶體佔用較少,我表示懷疑,但是沒有證據。

效能優化方法

瞭解語言的效能比較後,我們再看看一些效能優化的方法,這對在cpython和pypy之間選型有幫助。

使用c函數

python中使用c函數,比如這裡的累加可以使用reduce替換,可以提高效率:

def my_add(a, b):return a + bnumber = reduce(add, range(100000000))

結果展示,reduce對cpython和pypy都有效。

優化迴圈

優化最關鍵的地方,提高演算法效率,減少迴圈。更改一下累加的需求,假設我們是求100000000以內的偶數的和,下面展示了使用range的步進減少迴圈次數來提高效能:

try: xrange # python2注意使用xrange是迭代器,而range是返回一個listexcept NameError: # python3 xrange = rangedef test_0(): number = 0for i in range(100000000):if i % 2 == 0: number += ireturn numberdef test_1(): number = 0for i in xrange(0, 100000000, 2): number += ireturn number

迴圈次數減半後,有效率顯著提升。

靜態類型

python3可以使用類型註解,提高程式碼可讀性。類型確定邏輯上對效能有幫助,每次處理資料的時候,不用再進行類型推斷。

number: int = 0for i in range(100000000): number += i

記憶體相當於一個空間,我們要用不同的盒子去填充它。圖中左邊部分1,都使用長度為4(想像float類型)的盒子填充,一行一個,速度最快;圖中中間部分2,使用長度為3(想像long類型)和長度為1(想像int類型)的箱子,一行2個,也挺快;圖中右側3,雖然箱子長度仍然是3和1,但是由於沒有刻度,填充時候需要試裝,所以速度最慢。

資料類型演算法的魅力

優化到最後,最重量級的內容登場:高斯求和演算法。高斯的故事,想必大家都不陌生,下面是演算法實現:

def gaussian_sum(total: int) -> int:if total & 1 == 0:return (1 + total) * int(total / 2)else:return total * int((total - 1) / 2) + total# 4999999950000000number = gaussian_sum(100000000 - 1)

使用高斯求和後,程式秒開。這大概就是業內面試,要考演算法的真相,也是演算法的魅力所在。

優化的原則

簡單介紹一下優化的原則,主要是下面2點:

使用測試而不是推測。python3 -m timeit 'x=3''x%2'10000000 loops, best of 5: 25.3 nsec per looppython3 -m timeit 'x=3''x&1'5000000 loops, best of 5: 41.3 nsec per looppython2 -m timeit 'x=3''x&1'10000000 loops, best of 3: 0.0262 usec per looppython2 -m timeit 'x=3''x%2'10000000 loops, best of 3: 0.0371 usec per loop上面示例展示了,求奇偶的情況下,python3中位運算比取模慢,這是個反直覺推測的地方。在我的python冷兵器合集一文中也有介紹。而且需要注意的是,python2和python3表現相反,所以效能優化要實測,注意環境和實效性。

遵循2/8法則, 不要過度優化,不用贅述。pypy的特性

pypy還有下面一些特性:

cffi pypy推薦使用cffi的方式載入ccProfile pypy下使用cProfile檢測效能無效sys.getsizeof pypy的gc方式差異,sys.getsizeof無法使用__slots__ cpython使用的slots,在pypy下失效使用slots在python物件中,可以減少物件記憶體佔用,提高效率,下面是測試用例:

def test_0(): class Player(object): def __init__(self, name, age): self.name = name self.age = age players = []for i in range(10000): p = Player(name="p" + str(i), age=i) players.append(p)return playersdef test_1(): class Player(object): __slots__ = "name", "age" def __init__(self, name, age): self.name = name self.age = age players = []for i in range(10000): p = Player(name="p" + str(i), age=i) players.append(p)return players測試日誌如下:

# python3 slotspmem(rss=10776576, vms=5178499072, pfaults=3351, pageins=58)Elapsed time: 0.010818958282470703 s# python3 預設pmem(rss=11792384, vms=5033795584, pfaults=3587, pageins=0)Elapsed time: 0.01322031021118164 s# pypy3 slotspmem(rss=40042496, vms=5263011840, pfaults=12341, pageins=4071)Elapsed time: 0.005321025848388672 s# pypy3 預設pmem(rss=39862272, vms=4974653440, pfaults=12280, pageins=0)Elapsed time: 0.004619121551513672 s詳細資訊可以看參考連結4和5

pypy最重要的特性還是stackless,支援高併發。這裡有IO密集型任務(I/O-bound)和CPU密集型任務(compute-bound)的區分,CPU密集型任務的程式碼,速度很慢,是因為執行大量CPU指令,比如上文的for迴圈;I / O密集型,速度因磁碟或網路延遲而變慢,這兩者之間是有區別的。這部分內容,要介紹清楚也不容易,容我們下章見。

小結

python是一門解釋型程式語言,具有多種直譯器實現,常見的是cpython的實現。pypy使用了JIT技術,在一些常見的場景下可以顯著提高python的執行效率,對cpython的相容性也很高。如果項目純python部分較多,推薦嘗試使用pypy運行程式。

注:由於個人能力有限,文中示例如有謬誤,還望海涵。

附錄

測試環境

MacBook Pro (16-inch, 2019)(2.6 GHz 六核Intel Core i7)Python 2.7.16Python 3.8.5Python 3.6.9 [PyPy 7.3.1 with GCC 4.2.1 Compatible Apple LLVM 11.0.3 (clang-1103.0.32.59)]Python 2.7.13 [PyPy 7.3.1 with GCC 4.2.1 Compatible Apple LLVM 11.0.3 (clang-1103.0.32.59)]lua#Lua 5.2.3 Copyright (C) 1994-2013 Lua.org, PUC-Rionode#v10.16.3參考連結

https://medium.com/android-news/magic-lies-here-statically-typed-vs-dynamically-typed-languages-d151c7f95e2bhttps://realpython.com/pypy-faster-python/https://www.pypy.org/index.htmlhttps://stackoverflow.com/questions/23068076/using-slots-under-pypyhttps://morepypy.blogspot.com/2010/11/efficiently-implementing-python-objects.html

-------------------End -------------------


IT145.com E-mail:sddin#qq.com