最近 「pypy爲何能讓python比c還快」 刷屏了,原文講的內容偏理論,乾貨比較少。咱們能夠再深刻一點點,瞭解pypy的真相。php
正式開始以前,多嘮叨兩句。我司發力多個賽道的遊戲,其中包括某魚類遊戲Top2項目,拿過阿拉丁神燈獎的SLG卡牌小遊戲項目和海外三消遊戲。這些不一樣類型的遊戲,後端大可能是使用的是pypy。對於如何使用pypy,我有一點使用經驗能夠聊聊。話很少說,正式開始,本文包括下面幾個部分:html
咱們先從最基本的一些語言分類概念聊起,對這部份內容很是瞭解的朋友能夠跳過。java
若是在編譯時知道變量的類型,則該語言爲靜態類型。靜態類型語言的常見示例包括Java,C,C ++,FORTRAN,Pascal和Scala。在靜態類型語言中,一旦使用類型聲明瞭變量,就沒法將其分配給其餘不一樣類型的變量,這樣作會在編譯時引起類型錯誤。node
# java
int data;
data = 50;
data = 「Hello Game_404!」; // causes an compilation error
複製代碼
若是在運行時檢查變量的類型,則語言是動態類型的。動態類型語言的常見示例包括JavaScript,Objective-C,PHP,Python,Ruby,Lisp和Tcl。 在動態類型語言中,變量在運行時經過賦值語句綁定到對象,而且能夠在程序執行期間將相同的變量綁定到不一樣類型的對象。python
# python
data = 10;
data = "Hello Game_404!"; // no error caused
data = data + str(10)
複製代碼
通常來講靜態語言編譯成字節碼執行,動態語言使用解釋器執行。編譯型語言性能更高,可是較難移植到不一樣的CPU架構體系和操做系統。解釋型語言易於移植,性能會比編譯語言要差得多。這是頻譜的兩個極端。android
強類型語言是一種變量被綁定到特定數據類型的語言,若是類型與表達式中的預期不一致,將致使類型錯誤,好比下面這個:web
# 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;
複製代碼
常見編程語言的象限分類以下圖:算法
這一部份內容主要翻譯自參考連接1shell
python是一門動態編程語言,由特定的解釋器解釋執行。下面是一些解釋器實現:
還有幾個相關概念:
這裏你們會有一個疑問,python不是解釋型語言嘛?怎麼又有編譯後的pyc。是這樣的: py文件編譯成pyc後,解釋器默認 優先 執行pyc文件,這樣能夠加快python程序的 啓動速度 (注意是啓動速度)。繼背叛弱類型語言後,python這個鬼又在編譯語言和解釋語言之間橫跳。
還有一個事件是Go語言在1.5版本實現自舉。Go語言在1.5版本以前使用c實現的編譯器,在1.5版本時候使用Go實現了本身的編譯器,這裏有一個雞生蛋和蛋生雞的過程,也挺有意思。
pypy使用python的子集rpython實現瞭解釋器,和前面介紹的Go的自舉有點相似。反常識的是rpython的解釋器會比c實現的解釋器快? 主要是由於pypy使用了JIT技術。
Just-In-Time (JIT) Compiler 試圖經過對機器碼進行一些實際的編譯和一些解釋來得到一箭雙鵰的方法。簡而言之,如下是JIT編譯爲提升性能而採起的步驟:
這也是 「pypy爲何能讓python比c還快」 一文中的示例展示出來的能力。pypy除了速度快外,還有下面一些特色:
以上都是宣稱
pypy這麼強,快和省都佔了,爲何沒有大規模流行起來呢? 我我的認爲,主要仍是python的緣由。
須要注意的是,pypy同樣也有GIL的存在, 因此高併發主要在stackless。
這一部份內容參考自參考連接2
咱們能夠編寫性能測試用例,用代碼說話,對各個實現進行對比。本文的測試用例並不嚴謹,不過也足夠說明一些問題了。
原文中累加測試用例是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")
複製代碼
測試結果以下表(測試環境在本文附錄部分):
解釋器 | 循環次數 | 耗時(s) |
---|---|---|
python3 | 1000 | 0.00014281272888183594 |
pypy3 | 1000 | 0.00036716461181640625 |
結果顯示運行1000次循環的狀況下cpython要比pypy快,這和循環100000000次 相反 。用下面的例子能夠很是形象的解釋這一點。
假設您想去一家離您家很近的商店。您能夠步行或開車。您的汽車顯然比腳快得多。可是,請考慮須要執行如下操做:
開車要涉及不少開銷,若是您想去的地方在附近,這並不老是值得的!如今想一想若是您想去五十英里外的鄰近城市會發生什麼。開車去那裏而不是步行確定是值得的。
舉例來自參考連接2
儘管速度的差別並不像上面類比那麼明顯,可是PyPy和CPython的狀況也是如此。
咱們橫向對比一下c,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;
}
複製代碼
解釋器 | 循環次數 | 耗時(s) |
---|---|---|
c | 100000000 | 0.000 |
pypy3 | 100000000 | 0.15746307373046875 |
js | 100000000 | 0.198 |
lua | 100000000 | 0.8023 |
python3 | 100000000 | 10.14592313766479 |
測試結果可見,c無疑是最快的,秒殺其它語言,這是編譯語言的特色。在解釋語言中,pypy3表現配得上優秀二字。
測試用例中增長內存佔用的輸出:
p = psutil.Process()
mem = p.memory_info()
print(mem)
複製代碼
測試結果以下:
# python3
pmem(rss= 9027584, vms=4747534336, pfaults= 2914, pageins=1)
# pypy3
pmem(rss=39518208, vms=5127745536, pfaults=12188, pageins=58)
複製代碼
pypy3的內存佔用會比python3要高,這個才科學,用內存空間換了運行時間。固然這個評測並不嚴謹,實際狀況如何,pypy宣稱的內存佔用較少,我表示懷疑,可是沒有證據。
瞭解語言的性能比較後,咱們再看看一些性能優化的方法,這對在cpython和pypy之間選型有幫助。
python中使用c函數,好比這裏的累加可使用reduce替換,能夠提升效率:
def my_add(a, b):
return a + b
number = reduce(add, range(100000000))
複製代碼
解釋器 | 次數 | 耗時(s) |
---|---|---|
pypy3 | reduce | 0.08371400833129883 |
pypy3 | 100000000 | 0.15746307373046875 |
python3 | reduce | 5.705173015594482 s |
python3 | 100000000循環 | 10.14592313766479 |
結果展現,reduce對cpython和pypy都有效。
優化最關鍵的地方,提升算法效率,減小循環。更改一下累加的需求,假設咱們是求100000000之內的偶數的和,下面展現了使用range的步進減小循環次數來提升性能:
try:
xrange # python2注意使用xrange是迭代器,而range是返回一個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
複製代碼
解釋器 | 循環次數 | 耗時(s) |
---|---|---|
python3 | 50000000 | 2.6723649501800537 s |
python3 | 100000000 | 6.530670881271362 s |
循環次數減半後,有效率顯著提高。
python3可使用類型註解,提升代碼可讀性。類型肯定邏輯上對性能有幫助,每次處理數據的時候,不用再進行類型推斷。
number: int = 0
for i in range(100000000):
number += i
複製代碼
解釋器 | 循環次數 | 類型 | 耗時(s) |
---|---|---|---|
python3 | 100000000 | int | 9.492593050003052 s |
python3 | 100000000 | 不定義 | 10.14592313766479 s |
內存至關於一個空間,咱們要用不一樣的盒子去填充它。圖中左邊部分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
# 4999999950000000
number = gaussian_sum(100000000 - 1)
複製代碼
解釋器 | 循環次數 | 耗時(s) |
---|---|---|
python3 | 高斯求和 | 4.100799560546875e-05 s |
python3 | 100000000循環 | 10.14592313766479 |
使用高斯求和後,程序秒開。這大概就是業內面試,要考算法的真相,也是算法的魅力所在。
簡單介紹一下優化的原則,主要是下面2點:
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中位運算比取模慢,這是個反直覺推測的地方。在個人python冷兵器合集一文中也有介紹。並且須要注意的是,python2和python3表現相反,因此性能優化要實測,注意環境和實效性。
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 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
複製代碼
測試日誌以下:
# python3 slots
pmem(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 slots
pmem(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運行程序。
順便打個廣告,我司常年招聘各種技術研發,遊戲策劃,測試,產品經理等等,python方向感興趣的朋友能夠微信公衆號里加博主微信參加內推。
注:因爲我的能力有限,文中示例若有謬誤,還望海涵。