pypy真的能讓python比c還快?

最近 「pypy爲何能讓python比c還快」 刷屏了,原文講的內容偏理論,乾貨比較少。咱們能夠再深刻一點點,瞭解pypy的真相。php

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

  • 語言分類
  • python的解釋器實現
  • pypy爲何快
  • 性能比較
  • 性能優化方法
  • pypy的特性
  • 小結

語言分類

咱們先從最基本的一些語言分類概念聊起,對這部份內容很是瞭解的朋友能夠跳過。java

靜態語言 vs 動態語言

若是在編譯時知道變量的類型,則該語言爲靜態類型。靜態類型語言的常見示例包括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

強類型語言 vs 弱類型語言

強類型語言是一種變量被綁定到特定數據類型的語言,若是類型與表達式中的預期不一致,將致使類型錯誤,好比下面這個: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;
複製代碼

常見編程語言的象限分類以下圖:算法

language

這一部份內容主要翻譯自參考連接1shell

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編譯爲提升性能而採起的步驟:

  1. 標識代碼中最經常使用的組件,例如循環中的函數。
  2. 在運行時將這些零件轉換爲機器碼。
  3. 優化生成的機器碼。
  4. 用優化的機器碼版本交換之前的實現。

這也是 「pypy爲何能讓python比c還快」 一文中的示例展示出來的能力。pypy除了速度快外,還有下面一些特色:

  • 內存使用狀況比cpython少
  • gc策略更優化
  • Stackless 協程模式默認支持,支持高併發
  • 兼容性好,高度兼容cpython實現,基本能夠無縫切換

以上都是宣稱

pypy這麼強,快和省都佔了,爲何沒有大規模流行起來呢? 我我的認爲,主要仍是python的緣由。

  1. python生態中大量庫採用c實現,特別是科學計算/AI相關的庫,pypy在這塊並無優點。pypy快的主要在pure-python,也就是純粹的python實現部分。
  2. pypy適合長駐內存的高併發應用(web服務類)
  3. python是一門膠水語言,並不追求性能極致,即便快4倍也不夠快:( 🐶。確定比不上c,原文中的c應該是 偷換了概念 ,指c實現的cpython解釋器。

須要注意的是,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次 相反 。用下面的例子能夠很是形象的解釋這一點。

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

  1. 去你的車庫。
  2. 啓動你的車。
  3. 讓汽車暖一點。
  4. 開車去商店。
  5. 查找停車位。
  6. 在返回途中重複該過程。

開車要涉及不少開銷,若是您想去的地方在附近,這並不老是值得的!如今想一想若是您想去五十英里外的鄰近城市會發生什麼。開車去那裏而不是步行確定是值得的。

舉例來自參考連接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之間選型有幫助。

使用c函數

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點:

  1. 使用測試而不是推測。
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表現相反,因此性能優化要實測,注意環境和實效性。

  1. 遵循2/8法則, 不要過分優化,不用贅述。

pypy的特性

pypy還有下面一些特性:

  • cffi pypy推薦使用cffi的方式加載c
  • cProfile 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 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方向感興趣的朋友能夠微信公衆號里加博主微信參加內推。

注:因爲我的能力有限,文中示例若有謬誤,還望海涵。

附錄

測試環境

  • 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

參考連接

  1. medium.com/android-new…
  2. realpython.com/pypy-faster…
  3. www.pypy.org/index.html
  4. stackoverflow.com/questions/2…
  5. morepypy.blogspot.com/2010/11/eff…
相關文章
相關標籤/搜索