pypy真的能讓python比c還快?

作者 | 肖恩頓
來(lái)源 | 游戲不存在
最近 “pypy為什么能讓python比c還快” 刷屏了,真的是如此?先說(shuō)結(jié)論:pypy并不能讓python的執(zhí)行速度比c語(yǔ)言快,而是比cpython快!
本篇文章我們會(huì)講解:為什么pypy解釋器比默認(rèn)cpython解釋器快?既然快為什么將不pypy設(shè)置為默認(rèn)解釋器?還有實(shí)測(cè):c、cpython、pypy、js等執(zhí)行速度!
正式開(kāi)始之前,多嘮叨兩句。我司發(fā)力多個(gè)賽道的游戲,其中包括某魚(yú)類(lèi)游戲Top2項(xiàng)目,拿過(guò)阿拉丁神燈獎(jiǎng)的SLG卡牌小游戲項(xiàng)目和海外三消游戲。這些不同類(lèi)型的游戲,后端大多是使用的是pypy。對(duì)于如何使用pypy,我有一點(diǎn)使用經(jīng)驗(yàn)可以聊聊。話不多說(shuō),正式開(kāi)始,本文包括下面幾個(gè)部分:
1、語(yǔ)言分類(lèi) 2、python的解釋器實(shí)現(xiàn) 3、pypy為什么快 4、性能比較 5、性能優(yōu)化方法 6、pypy的特性 7、小結(jié)
一、語(yǔ)言分類(lèi)
我們先從最基本的一些語(yǔ)言分類(lèi)概念聊起,對(duì)這部分內(nèi)容非常了解的朋友可以跳過(guò)。
1.靜態(tài)語(yǔ)言 vs 動(dòng)態(tài)語(yǔ)言
如果在編譯時(shí)知道變量的類(lèi)型,則該語(yǔ)言為靜態(tài)類(lèi)型。靜態(tài)類(lèi)型語(yǔ)言的常見(jiàn)示例包括Java,C,C ++,F(xiàn)ORTRAN,Pascal和Scala。在靜態(tài)類(lèi)型語(yǔ)言中,一旦使用類(lèi)型聲明了變量,就無(wú)法將其分配給其他不同類(lèi)型的變量,這樣做會(huì)在編譯時(shí)引發(fā)類(lèi)型錯(cuò)誤。
# java
int data;
data = 50;
data = “Hello Game_404!”; // causes an compilation error
如果在運(yùn)行時(shí)檢查變量的類(lèi)型,則語(yǔ)言是動(dòng)態(tài)類(lèi)型的。動(dòng)態(tài)類(lèi)型語(yǔ)言的常見(jiàn)示例包括JavaScript,Objective-C,PHP,Python,Ruby,Lisp和Tcl。在動(dòng)態(tài)類(lèi)型語(yǔ)言中,變量在運(yùn)行時(shí)通過(guò)賦值語(yǔ)句綁定到對(duì)象,并且可以在程序執(zhí)行期間將相同的變量綁定到不同類(lèi)型的對(duì)象。
# python
data = 10;
data = "Hello Game_404!"; // no error caused
data = data + str(10)
一般來(lái)說(shuō)靜態(tài)語(yǔ)言編譯成字節(jié)碼執(zhí)行,動(dòng)態(tài)語(yǔ)言使用解釋器執(zhí)行。編譯型語(yǔ)言性能更高,但是較難移植到不同的CPU架構(gòu)體系和操作系統(tǒng)。解釋型語(yǔ)言易于移植,性能會(huì)比編譯語(yǔ)言要差得多。這是頻譜的兩個(gè)極端。
2.強(qiáng)類(lèi)型語(yǔ)言 vs 弱類(lèi)型語(yǔ)言
強(qiáng)類(lèi)型語(yǔ)言是一種變量被綁定到特定數(shù)據(jù)類(lèi)型的語(yǔ)言,如果類(lèi)型與表達(dá)式中的預(yù)期不一致,將導(dǎo)致類(lèi)型錯(cuò)誤,比如下面這個(gè):
# python
temp = “Hello Game_404!”
temp = temp + 10; // program terminates with below stated error (TypeError: must be str, not int)
python和我們感覺(jué)不一致,背叛了弱類(lèi)型語(yǔ)言,不像世界上最好的語(yǔ)言:(
# php
$temp = “Hello Game_404!”;
$temp = $temp + 10; // no error caused
echo $temp;
常見(jiàn)編程語(yǔ)言的象限分類(lèi)如下圖:

這一部分內(nèi)容主要翻譯自參考鏈接1
二、python的解釋器實(shí)現(xiàn)
python是一門(mén)動(dòng)態(tài)編程語(yǔ)言,由特定的解釋器解釋執(zhí)行。下面是一些解釋器實(shí)現(xiàn):
CPython 使用c語(yǔ)言實(shí)現(xiàn)的解釋器 PyPy 使用python語(yǔ)言的子集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ù)),無(wú)需進(jìn)行開(kāi)發(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è)疑問(wèn),python不是解釋型語(yǔ)言嘛?怎么又有編譯后的pyc?
是這樣的: py文件編譯成pyc后,解釋器默認(rèn) 優(yōu)先 執(zhí)行pyc文件,這樣可以加快python程序的 啟動(dòng)速度 (注意是啟動(dòng)速度)。繼背叛弱類(lèi)型語(yǔ)言后,python這個(gè)鬼又在編譯語(yǔ)言和解釋語(yǔ)言之間橫跳。
三、pypy為什么快
pypy使用python的子集rpython實(shí)現(xiàn)了解釋器。反常識(shí)的是rpython的解釋器會(huì)比c實(shí)現(xiàn)的解釋器快?主要是因?yàn)閜ypy使用了JIT技術(shù)。
Just-In-Time (JIT) Compiler 試圖通過(guò)對(duì)機(jī)器碼進(jìn)行一些實(shí)際的編譯和一些解釋來(lái)獲得兩全其美的方法。簡(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)出來(lái)的能力。pypy除了速度快外,還有下面一些特點(diǎn):
內(nèi)存使用情況比cpython少 gc策略更優(yōu)化 Stackless 協(xié)程模式默認(rèn)支持,支持高并發(fā) 兼容性好,高度兼容cpython實(shí)現(xiàn),基本可以無(wú)縫切換
以上都是宣稱(chēng)
pypy這么強(qiáng),快和省都占了,為什么沒(méi)有大規(guī)模流行起來(lái)呢? 我個(gè)人認(rèn)為,主要還是python的原因。
python生態(tài)中大量庫(kù)采用c實(shí)現(xiàn),特別是科學(xué)計(jì)算/AI相關(guān)的庫(kù),pypy在這塊并沒(méi)有優(yōu)勢(shì)。pypy快的主要在pure-python,也就是純粹的python實(shí)現(xiàn)部分。 pypy適合長(zhǎng)駐內(nèi)存的高并發(fā)應(yīng)用(web服務(wù)類(lèi)) python是一門(mén)膠水語(yǔ)言,并不追求性能極致,即使快4倍也不夠快。肯定比不上c,原文中的c應(yīng)該是偷換了概念,指c實(shí)現(xiàn)的cpython解釋器。
需要注意的是,pypy一樣也有GIL的存在, 所以高并發(fā)主要在stackless。
這一部分內(nèi)容參考自參考鏈接2
四、性能比較
我們可以編寫(xiě)性能測(cè)試用例,用代碼說(shuō)話,對(duì)各個(gè)實(shí)現(xiàn)進(jìn)行對(duì)比。本文的測(cè)試用例并不嚴(yán)謹(jǐn),不過(guò)也足夠說(shuō)明一些問(wèn)題了。
1.開(kāi)車(chē)和步行
原文中累加測(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è)您想去一家離您家很近的商店。您可以步行或開(kāi)車(chē)。您的汽車(chē)顯然比腳快得多。但是,請(qǐng)考慮需要執(zhí)行以下操作:
去你的車(chē)庫(kù)。 啟動(dòng)你的車(chē)。 讓汽車(chē)暖一點(diǎn)。 開(kāi)車(chē)去商店。 查找停車(chē)位。 在返回途中重復(fù)該過(guò)程。
開(kāi)車(chē)要涉及很多開(kāi)銷(xiāo),如果您想去的地方在附近,這并不總是值得的!現(xiàn)在想想如果您想去五十英里外的鄰近城市會(huì)發(fā)生什么。開(kāi)車(chē)去那里而不是步行肯定是值得的。
舉例來(lái)自參考鏈接2
盡管速度的差異并不像上面類(lèi)比那么明顯,但是PyPy和CPython的情況也是如此。
2.橫向?qū)Ρ?/span>
我們橫向?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é)果可見(jiàn),c無(wú)疑是最快的,秒殺其它語(yǔ)言,這是編譯語(yǔ)言的特點(diǎn)。在解釋語(yǔ)言中,pypy3表現(xiàn)配得上優(yōu)秀二字。
3.內(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宣稱(chēng)的內(nèi)存占用較少,我表示懷疑,但是沒(méi)有證據(jù)。
五、性能優(yōu)化方法
了解語(yǔ)言的性能比較后,我們?cè)倏纯匆恍┬阅軆?yōu)化的方法,這對(duì)在cpython和pypy之間選型有幫助。
1.使用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都有效。
2.優(yōu)化循環(huán)
優(yōu)化最關(guān)鍵的地方,提高算法效率,減少循環(huán)。更改一下累加的需求,假設(shè)我們是求100000000以?xún)?nèi)的偶數(shù)的和,下面展示了使用range的步進(jìn)減少循環(huán)次數(shù)來(lái)提高性能:
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ù)減半后,有效率顯著提升。
3.靜態(tài)類(lèi)型
python3可以使用類(lèi)型注解,提高代碼可讀性。類(lèi)型確定邏輯上對(duì)性能有幫助,每次處理數(shù)據(jù)的時(shí)候,不用再進(jìn)行類(lèi)型推斷。
number: int = 0
for i in range(100000000):
number += i
| 解釋器 | 循環(huán)次數(shù) | 類(lèi)型 | 耗時(shí)(s) |
|---|---|---|---|
| python3 | 100000000 | int | 9.492593050003052 s |
| python3 | 100000000 | 不定義 | 10.14592313766479 s |
內(nèi)存相當(dāng)于一個(gè)空間,我們要用不同的盒子去填充它。圖中左邊部分1,都使用長(zhǎng)度為4(想像float類(lèi)型)的盒子填充,一行一個(gè),速度最快;圖中中間部分2,使用長(zhǎng)度為3(想像long類(lèi)型)和長(zhǎng)度為1(想像int類(lèi)型)的箱子,一行2個(gè),也挺快;圖中右側(cè)3,雖然箱子長(zhǎng)度仍然是3和1,但是由于沒(méi)有刻度,填充時(shí)候需要試裝,所以速度最慢。

4.算法的魅力
優(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 |
使用高斯求和后,程序秒開(kāi)。這大概就是業(yè)內(nèi)面試,要考算法的真相,也是算法的魅力所在。
5.優(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è)反直覺(jué)推測(cè)的地方。在我的python冷兵器合集一文中也有介紹。而且需要注意的是,python2和python3表現(xiàn)相反,所以性能優(yōu)化要實(shí)測(cè),注意環(huán)境和實(shí)效性。
遵循2/8法則, 不要過(guò)度優(yōu)化,不用贅述。
六、pypy的特性
pypy還有下面一些特性:
cffi pypy推薦使用cffi的方式加載c cProfile pypy下使用cProfile檢測(cè)性能無(wú)效 sys.getsizeof pypy的gc方式差異,sys.getsizeof無(wú)法使用 __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密集型,速度因磁盤(pán)或網(wǎng)絡(luò)延遲而變慢,這兩者之間是有區(qū)別的。這部分內(nèi)容,要介紹清楚也不容易,容我們下章見(jiàn)。
七、小結(jié)
python是一門(mén)解釋型編程語(yǔ)言,具有多種解釋器實(shí)現(xiàn),常見(jiàn)的是cpython的實(shí)現(xiàn)。pypy使用了JIT技術(shù),在一些常見(jiàn)的場(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
