pypy真的能讓python比c還快?
回復(fù)“書籍”即可獲贈(zèng)Python從入門到進(jìn)階共10本電子書
最近 “pypy為什么能讓python比c還快” 刷屏了,原文講的內(nèi)容偏理論,干貨比較少。我們可以再深入一點(diǎn)點(diǎn),了解pypy的真相。
正式開始之前,多嘮叨兩句。我司發(fā)力多個(gè)賽道的游戲,其中包括某魚類游戲Top2項(xiàng)目,拿過阿拉丁神燈獎(jiǎng)的SLG卡牌小游戲項(xiàng)目和海外三消游戲。這些不同類型的游戲,后端大多是使用的是pypy。對(duì)于如何使用pypy,我有一點(diǎn)使用經(jīng)驗(yàn)可以聊聊。話不多說,正式開始,本文包括下面幾個(gè)部分:
語言分類 python的解釋器實(shí)現(xiàn) pypy為什么快 性能比較 性能優(yōu)化方法 pypy的特性 小結(jié)
語言分類
我們先從最基本的一些語言分類概念聊起,對(duì)這部分內(nèi)容非常了解的朋友可以跳過。
靜態(tài)語言 vs 動(dòng)態(tài)語言
如果在編譯時(shí)知道變量的類型,則該語言為靜態(tài)類型。靜態(tài)類型語言的常見示例包括Java,C,C ++,F(xiàn)ORTRAN,Pascal和Scala。在靜態(tài)類型語言中,一旦使用類型聲明了變量,就無法將其分配給其他不同類型的變量,這樣做會(huì)在編譯時(shí)引發(fā)類型錯(cuò)誤。
# java
int data;
data = 50;
data = “Hello Game_404!”; // causes an compilation error
如果在運(yùn)行時(shí)檢查變量的類型,則語言是動(dòng)態(tài)類型的。動(dòng)態(tài)類型語言的常見示例包括JavaScript,Objective-C,PHP,Python,Ruby,Lisp和Tcl。在動(dòng)態(tài)類型語言中,變量在運(yùn)行時(shí)通過賦值語句綁定到對(duì)象,并且可以在程序執(zhí)行期間將相同的變量綁定到不同類型的對(duì)象。
# python
data = 10;
data = "Hello Game_404!"; // no error caused
data = data + str(10)
一般來說靜態(tài)語言編譯成字節(jié)碼執(zhí)行,動(dòng)態(tài)語言使用解釋器執(zhí)行。編譯型語言性能更高,但是較難移植到不同的CPU架構(gòu)體系和操作系統(tǒng)。解釋型語言易于移植,性能會(huì)比編譯語言要差得多。這是頻譜的兩個(gè)極端。
強(qiáng)類型語言 vs 弱類型語言
強(qiáng)類型語言是一種變量被綁定到特定數(shù)據(jù)類型的語言,如果類型與表達(dá)式中的預(yù)期不一致,將導(dǎo)致類型錯(cuò)誤,比如下面這個(gè):
# python
temp = “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 caused
echo $temp;
常見編程語言的象限分類如下圖:

這一部分內(nèi)容主要翻譯自參考鏈接1
python的解釋器實(shí)現(xiàn)
python是一門動(dòng)態(tài)編程語言,由特定的解釋器解釋執(zhí)行。下面是一些解釋器實(shí)現(xiàn):
CPython 使用c語言實(shí)現(xiàn)的解釋器 PyPy 使用python語言的子集RPython實(shí)現(xiàn)的解釋器,一般情況下PyPy比CPython快4.2倍 Stackless Python 帶有協(xié)程實(shí)現(xiàn)的解釋器 Jython Java實(shí)現(xiàn)的解釋器 IronPython .net實(shí)現(xiàn)的解釋器 Pyston 一個(gè)較新的實(shí)現(xiàn),是CPython 3.8.8的一個(gè)分支,具有其他針對(duì)性能的優(yōu)化。它針對(duì)大型現(xiàn)實(shí)應(yīng)用程序(例如Web服務(wù)),無需進(jìn)行開發(fā)工作即可提供高達(dá)30%的加速。 ...
還有幾個(gè)相關(guān)概念:
IPython && Jupyter ipython是使用python構(gòu)建的交互式shell, Jupyter是其web化的包裝。 Anaconda 是一個(gè)python虛擬環(huán)境,Python數(shù)據(jù)科學(xué)常用。 mypyc 一個(gè)新的項(xiàng)目,將python編譯成c代碼庫(kù),以期提高python的運(yùn)行效率。 py文件和pyc文件 pyc文件是python編譯后的字節(jié)碼,也可以由python解釋器執(zhí)行。 wheel文件和egg文件 都是項(xiàng)目版本發(fā)布的打包文件,wheel是最新標(biāo)準(zhǔn)。 ...
這里大家會(huì)有一個(gè)疑問,python不是解釋型語言嘛?怎么又有編譯后的pyc。是這樣的: py文件編譯成pyc后,解釋器默認(rèn) 優(yōu)先 執(zhí)行pyc文件,這樣可以加快python程序的 啟動(dòng)速度 (注意是啟動(dòng)速度)。繼背叛弱類型語言后,python這個(gè)鬼又在編譯語言和解釋語言之間橫跳。
還有一個(gè)事件是Go語言在1.5版本實(shí)現(xiàn)自舉。Go語言在1.5版本之前使用c實(shí)現(xiàn)的編譯器,在1.5版本時(shí)候使用Go實(shí)現(xiàn)了自己的編譯器,這里有一個(gè)雞生蛋和蛋生雞的過程,也挺有意思。
pypy為什么快
pypy使用python的子集rpython實(shí)現(xiàn)了解釋器,和前面介紹的Go的自舉有點(diǎn)類似。反常識(shí)的是rpython的解釋器會(huì)比c實(shí)現(xiàn)的解釋器快?主要是因?yàn)閜ypy使用了JIT技術(shù)。
Just-In-Time (JIT) Compiler 試圖通過對(duì)機(jī)器碼進(jìn)行一些實(shí)際的編譯和一些解釋來獲得兩全其美的方法。簡(jiǎn)而言之,以下是JIT編譯為提高性能而采取的步驟:
標(biāo)識(shí)代碼中最常用的組件,例如循環(huán)中的函數(shù)。 在運(yùn)行時(shí)將這些零件轉(zhuǎn)換為機(jī)器碼。 優(yōu)化生成的機(jī)器碼。 用優(yōu)化的機(jī)器碼版本交換以前的實(shí)現(xiàn)。
這也是 “pypy為什么能讓python比c還快” 一文中的示例展現(xiàn)出來的能力。pypy除了速度快外,還有下面一些特點(diǎn):
內(nèi)存使用情況比cpython少 gc策略更優(yōu)化 Stackless 協(xié)程模式默認(rèn)支持,支持高并發(fā) 兼容性好,高度兼容cpython實(shí)現(xiàn),基本可以無縫切換
以上都是宣稱
pypy這么強(qiáng),快和省都占了,為什么沒有大規(guī)模流行起來呢? 我個(gè)人認(rèn)為,主要還是python的原因。
python生態(tài)中大量庫(kù)采用c實(shí)現(xiàn),特別是科學(xué)計(jì)算/AI相關(guān)的庫(kù),pypy在這塊并沒有優(yōu)勢(shì)。pypy快的主要在pure-python,也就是純粹的python實(shí)現(xiàn)部分。 pypy適合長(zhǎng)駐內(nèi)存的高并發(fā)應(yīng)用(web服務(wù)類) python是一門膠水語言,并不追求性能極致,即使快4倍也不夠快:( ????隙ū炔簧蟘,原文中的c應(yīng)該是 偷換了概念 ,指c實(shí)現(xiàn)的cpython解釋器。
需要注意的是,pypy一樣也有GIL的存在, 所以高并發(fā)主要在stackless。
這一部分內(nèi)容參考自參考鏈接2
性能比較
我們可以編寫性能測(cè)試用例,用代碼說話,對(duì)各個(gè)實(shí)現(xiàn)進(jìn)行對(duì)比。本文的測(cè)試用例并不嚴(yán)謹(jǐn),不過也足夠說明一些問題了。
開車和步行
原文中累加測(cè)試用例是100000000次,我們減少成1000次:
import time
start = time.time()
number = 0
for i in range(1000):
number += i
print(number)
print(f"Elapsed time: {time.time() - start} s")
測(cè)試結(jié)果如下表(測(cè)試環(huán)境在本文附錄部分):
| 解釋器 | 循環(huán)次數(shù) | 耗時(shí)(s) |
|---|---|---|
| python3 | 1000 | 0.00014281272888183594 |
| pypy3 | 1000 | 0.00036716461181640625 |
結(jié)果顯示運(yùn)行1000次循環(huán)的情況下cpython要比pypy快,這和循環(huán)100000000次 相反 。用下面的例子可以非常形象的解釋這一點(diǎn)。
假設(shè)您想去一家離您家很近的商店。您可以步行或開車。您的汽車顯然比腳快得多。但是,請(qǐng)考慮需要執(zhí)行以下操作:
去你的車庫(kù)。 啟動(dòng)你的車。 讓汽車暖一點(diǎn)。 開車去商店。 查找停車位。 在返回途中重復(fù)該過程。
開車要涉及很多開銷,如果您想去的地方在附近,這并不總是值得的!現(xiàn)在想想如果您想去五十英里外的鄰近城市會(huì)發(fā)生什么。開車去那里而不是步行肯定是值得的。
舉例來自參考鏈接2
盡管速度的差異并不像上面類比那么明顯,但是PyPy和CPython的情況也是如此。
橫向?qū)Ρ?span style="display: none;">
我們橫向?qū)Ρ纫幌耤,python3, pypy3, js 和lua的性能。
# js
const start = Date.now();
let number = 0
for (i=0;i<100000000;i++){
number += i
}
console.log(number)
const millis = Date.now() - start;
console.log(`milliseconds elapsed = `, millis);
# lua
local starttime = os.clock();
local number = 0
local total = 100000000-1
for i=total,1,-1 do
number = number+i
end
print(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;
}
| 解釋器 | 循環(huán)次數(shù) | 耗時(shí)(s) |
|---|---|---|
| c | 100000000 | 0.000 |
| pypy3 | 100000000 | 0.15746307373046875 |
| js | 100000000 | 0.198 |
| lua | 100000000 | 0.8023 |
| python3 | 100000000 | 10.14592313766479 |
測(cè)試結(jié)果可見,c無疑是最快的,秒殺其它語言,這是編譯語言的特點(diǎn)。在解釋語言中,pypy3表現(xiàn)配得上優(yōu)秀二字。
內(nèi)存占用
測(cè)試用例中增加內(nèi)存占用的輸出:
p = psutil.Process()
mem = p.memory_info()
print(mem)
測(cè)試結(jié)果如下:
# python3
pmem(rss= 9027584, vms=4747534336, pfaults= 2914, pageins=1)
# pypy3
pmem(rss=39518208, vms=5127745536, pfaults=12188, pageins=58)
pypy3的內(nèi)存占用會(huì)比python3要高,這個(gè)才科學(xué),用內(nèi)存空間換了運(yùn)行時(shí)間。當(dāng)然這個(gè)評(píng)測(cè)并不嚴(yán)謹(jǐn),實(shí)際情況如何,pypy宣稱的內(nèi)存占用較少,我表示懷疑,但是沒有證據(jù)。
性能優(yōu)化方法
了解語言的性能比較后,我們?cè)倏纯匆恍┬阅軆?yōu)化的方法,這對(duì)在cpython和pypy之間選型有幫助。
使用c函數(shù)
python中使用c函數(shù),比如這里的累加可以使用reduce替換,可以提高效率:
def my_add(a, b):
return a + b
number = reduce(add, range(100000000))
| 解釋器 | 次數(shù) | 耗時(shí)(s) |
|---|---|---|
| pypy3 | reduce | 0.08371400833129883 |
| pypy3 | 100000000 | 0.15746307373046875 |
| python3 | reduce | 5.705173015594482 s |
| python3 | 100000000循環(huán) | 10.14592313766479 |
結(jié)果展示,reduce對(duì)cpython和pypy都有效。
優(yōu)化循環(huán)
優(yōu)化最關(guān)鍵的地方,提高算法效率,減少循環(huán)。更改一下累加的需求,假設(shè)我們是求100000000以內(nèi)的偶數(shù)的和,下面展示了使用range的步進(jìn)減少循環(huán)次數(shù)來提高性能:
try:
xrange # python2注意使用xrange是迭代器,而range是返回一個(gè)list
except NameError: # python3
xrange = range
def test_0():
number = 0
for i in range(100000000):
if i % 2 == 0:
number += i
return number
def test_1():
number = 0
for i in xrange(0, 100000000, 2):
number += i
return number
| 解釋器 | 循環(huán)次數(shù) | 耗時(shí)(s) |
|---|---|---|
| python3 | 50000000 | 2.6723649501800537 s |
| python3 | 100000000 | 6.530670881271362 s |
循環(huán)次數(shù)減半后,有效率顯著提升。
靜態(tài)類型
python3可以使用類型注解,提高代碼可讀性。類型確定邏輯上對(duì)性能有幫助,每次處理數(shù)據(jù)的時(shí)候,不用再進(jìn)行類型推斷。
number: int = 0
for i in range(100000000):
number += i
| 解釋器 | 循環(huán)次數(shù) | 類型 | 耗時(shí)(s) |
|---|---|---|---|
| python3 | 100000000 | int | 9.492593050003052 s |
| python3 | 100000000 | 不定義 | 10.14592313766479 s |
內(nèi)存相當(dāng)于一個(gè)空間,我們要用不同的盒子去填充它。圖中左邊部分1,都使用長(zhǎng)度為4(想像float類型)的盒子填充,一行一個(gè),速度最快;圖中中間部分2,使用長(zhǎng)度為3(想像long類型)和長(zhǎng)度為1(想像int類型)的箱子,一行2個(gè),也挺快;圖中右側(cè)3,雖然箱子長(zhǎng)度仍然是3和1,但是由于沒有刻度,填充時(shí)候需要試裝,所以速度最慢。

算法的魅力
優(yōu)化到最后,最重量級(jí)的內(nèi)容登場(chǎng):高斯求和算法。高斯的故事,想必大家都不陌生,下面是算法實(shí)現(xiàn):
def gaussian_sum(total: int) -> int:
if total & 1 == 0:
return (1 + total) * int(total / 2)
else:
return total * int((total - 1) / 2) + total
# 4999999950000000
number = gaussian_sum(100000000 - 1)
| 解釋器 | 循環(huán)次數(shù) | 耗時(shí)(s) |
|---|---|---|
| python3 | 高斯求和 | 4.100799560546875e-05 s |
| python3 | 100000000循環(huán) | 10.14592313766479 |
使用高斯求和后,程序秒開。這大概就是業(yè)內(nèi)面試,要考算法的真相,也是算法的魅力所在。
優(yōu)化的原則
簡(jiǎn)單介紹一下優(yōu)化的原則,主要是下面2點(diǎn):
使用測(cè)試而不是推測(cè)。
python3 -m timeit 'x=3' 'x%2'
10000000 loops, best of 5: 25.3 nsec per loop
python3 -m timeit 'x=3' 'x&1'
5000000 loops, best of 5: 41.3 nsec per loop
python2 -m timeit 'x=3' 'x&1'
10000000 loops, best of 3: 0.0262 usec per loop
python2 -m timeit 'x=3' 'x%2'
10000000 loops, best of 3: 0.0371 usec per loop
上面示例展示了,求奇偶的情況下,python3中位運(yùn)算比取模慢,這是個(gè)反直覺推測(cè)的地方。在我的python冷兵器合集一文中也有介紹。而且需要注意的是,python2和python3表現(xiàn)相反,所以性能優(yōu)化要實(shí)測(cè),注意環(huán)境和實(shí)效性。
遵循2/8法則, 不要過度優(yōu)化,不用贅述。
pypy的特性
pypy還有下面一些特性:
cffi pypy推薦使用cffi的方式加載c cProfile pypy下使用cProfile檢測(cè)性能無效 sys.getsizeof pypy的gc方式差異,sys.getsizeof無法使用 __slots__ cpython使用的slots,在pypy下失效
使用slots在python對(duì)象中,可以減少對(duì)象內(nèi)存占用,提高效率,下面是測(cè)試用例:
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 players
def 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
測(cè)試日志如下:
# python3 slots
pmem(rss=10776576, vms=5178499072, pfaults=3351, pageins=58)
Elapsed time: 0.010818958282470703 s
# python3 默認(rèn)
pmem(rss=11792384, vms=5033795584, pfaults=3587, pageins=0)
Elapsed time: 0.01322031021118164 s
# pypy3 slots
pmem(rss=40042496, vms=5263011840, pfaults=12341, pageins=4071)
Elapsed time: 0.005321025848388672 s
# pypy3 默認(rèn)
pmem(rss=39862272, vms=4974653440, pfaults=12280, pageins=0)
Elapsed time: 0.004619121551513672 s
詳細(xì)信息可以看參考鏈接4和5
pypy最重要的特性還是stackless,支持高并發(fā)。這里有IO密集型任務(wù)(I/O-bound)和CPU密集型任務(wù)(compute-bound)的區(qū)分,CPU密集型任務(wù)的代碼,速度很慢,是因?yàn)閳?zhí)行大量CPU指令,比如上文的for循環(huán);I / O密集型,速度因磁盤或網(wǎng)絡(luò)延遲而變慢,這兩者之間是有區(qū)別的。這部分內(nèi)容,要介紹清楚也不容易,容我們下章見。
小結(jié)
python是一門解釋型編程語言,具有多種解釋器實(shí)現(xiàn),常見的是cpython的實(shí)現(xiàn)。pypy使用了JIT技術(shù),在一些常見的場(chǎng)景下可以顯著提高python的執(zhí)行效率,對(duì)cpython的兼容性也很高。如果項(xiàng)目純python部分較多,推薦嘗試使用pypy運(yùn)行程序。
注:由于個(gè)人能力有限,文中示例如有謬誤,還望海涵。
附錄
測(cè)試環(huán)境
MacBook Pro (16-inch, 2019)(2.6 GHz 六核Intel Core i7) Python 2.7.16 Python 3.8.5 Python 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-Rio node#v10.16.3
參考鏈接
https://medium.com/android-news/magic-lies-here-statically-typed-vs-dynamically-typed-languages-d151c7f95e2b https://realpython.com/pypy-faster-python/ https://www.pypy.org/index.html https://stackoverflow.com/questions/23068076/using-slots-under-pypy https://morepypy.blogspot.com/2010/11/efficiently-implementing-python-objects.html
------------------- End -------------------
往期精彩文章推薦:
一篇文章帶你全面解析不一樣的線程
一篇文章帶你了解Python中的游戲開發(fā)模塊pyglet
手把手教你用Scrapy爬蟲框架爬取食品論壇數(shù)據(jù)并存入數(shù)據(jù)庫(kù)

歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Python學(xué)習(xí)群請(qǐng)?jiān)诤笈_(tái)回復(fù)【入群】
萬水千山總是情,點(diǎn)個(gè)【在看】行不行
/今日留言主題/
隨便說一兩句吧~~
