Pypy從表面意思上面來說的話,就是用Python實現的Python。但是更准確的描述應該是RPython實現的Python。
RPython是Python的子集,為什么到現在CPython一直沒有加入JIT功能,就是因為它的變量的類型是運行時確定的,也正是因為這樣,JIT很難做。
x = random.choice([1, "foo"])
在編譯期,很難確定這是x是什么類型的,所以JIT優化很難做,這個時候就使用了RPython來實現Python的語法了,但是它具有靜態類型,這樣JIT實現起來更加容易。
RPython實現的是Python代碼的解析器,所以它只是一個編譯器的前端,那么編譯器的后端是什么呢?目前Pypy只實現了Python到C的編譯,也就是說編譯器的后端實現了直接轉成了機器碼。
當然,編譯器后端也可以編譯成Java字節碼,C#字節碼。這個只是暫時沒有實現而已。
Pypy之所以難以理解,就是因為很多程序員並沒有學習過編譯原理,簡單介紹下,
1.編譯前期,就相當於詞法分析器,把源代碼分析成中間格式,這種格式是不能執行的,需要進入第2個步驟
2.編譯后期生成的文件就相當於字節碼,生成了Java字節碼就能在JVM上面執行,生成了.Net字節碼就能在CLR虛擬機上面運行
Pypy和上面做一下對比就清楚了。
1.RPython的功能就是實現詞法的分析
2.Pypy會根據RPython生成的中間文件,進行后期編譯,因為RPython具有的靜態類型,實現JIT更簡單,這也是為什么使用RPython的原因
以上的部分描述並不是特別准確,但是通過簡單的描述,可以更加方便的理解Pypy的原理。下面列出來一寫參考。
對PyPy的偉大的工作人員和Emscripten的開發人員來說,現實中混合這兩種技術幾乎同理論上聽起來那樣容易。PyPy的RPython工具鏈有 一個可以讓你很容易地插入定制的編譯器或者甚至插入一個完整的新工具鏈的擴展的地方。我的github分支就包含把它和Emscripten掛接所必須的 邏輯:
Emscripten努力做到像標准的Posix構建鏈那樣運行,這樣只要求你用"emcc"替換通常所用的"gcc"調用。我確實需要做一點調整使它更像Posix運行環境,因此你需要用到下面的分支,直到它們與上面的分支合並為止:
https://github.com/rpk/emscripten
為了把RPython代碼編譯為常見的可執行包,你要調用"rpython"轉換程序。下面是一個從PyPy源代碼倉庫中提取簡單的“你好,世界”的例子,它可以直接運行:
$> python ./rpython/bin/rpython ./rpython/translator/goal/targetnopstandalone.py [...lots and lots of compiler output...] $> $> ./targetnopstandalone-c debug: hello world $>
然而為了把RPython代碼編譯成JavaScript,你只需要指定選項"--backend=js"。生成的JavaScript文件可以使用如nodejs這樣的JavaScript shell的命令行來執行:
$> python ./rpython/bin/rpython --backend=js ./rpython/translator/goal/targetnopstandalone.py [...lots and lots of compiler output...] $> $> node ./targetnopstandalone-js debug: hello world $>
這就是所有要做的。如果你還有多余時間的話,那么你可以執行下面命令把整個PyPy解釋器轉換為JavaScript:
> python ./rpython/bin/rpython --backend=js --opt=2 ./pypy/goal/targetpypystandalone.py [...seriously, this will take forever...] ^C $>
或者你只想獲取最終的結果: pypy.js
未壓縮的情況下生成的JavaScript文件為139M.它包括完整的Python語言解釋器,幾個非常重要的內置模塊以及附加的Python標准庫中 所有.py文件的列表。如果你手邊有一個JavaScrip shell的話,你可以像下面命令行這樣傳遞這些參數JavaScript shell來運行Python命令:
$> node pypy.js -c 'print "HELLO WORLD"' debug: WARNING: Library path not found, using compiled-in sys.path. debug: WARNING: 'sys.prefix' will not be set. debug: WARNING: Make sure the pypy binary is kept inside its tree of files. debug: WARNING: It is ok to create a symlink to it from somewhere else. 'import site' failed HELLO WORLD $>
正如你所料,第一個版本有非常多的警告:
- 沒有即時編譯器(JIT)。在上面,我通過傳遞"--opt=2"選項顯式地禁止了省城即時編譯器。生成即時編譯器需要一些平台相關的代碼的支持,實際上我仍然沒有弄清楚它應該看起來像什么。
- 沒有文件系統訪問權限,這使得在啟動的時候就打印出調試的告警信息。這還需要做些對Emscripten擴展可插拔虛擬文件系統的工作,在將來的某個時刻可啟用本地文件的訪問權限。
- 然而,為了提供Python標准庫,它使用了綁定文件系統的快照。這使得啟動非常非常地慢,因為在進入解釋器的主循環之前需要把整個快照解包到內存。
- 沒有交互式控制台。輸出運行正常,不過輸入卻並不是這樣的。我仍然不想深挖細節,不過讓一些基本的東西運行應該不是太難的。
- 丟失了許多內置模塊,因為這些內置模塊需要其他C級別的依賴。比如,"hashlib"模塊依賴OpenSSL。我將一個接着一個地添加這些內置模塊。
- 我肯定不會像repl.it那樣在它的上面放一個基於瀏覽器的華麗的用戶界面(UI)。
因此即便沒有這些,你也不可能立刻在瀏覽器里運行這個。不過它是真正的Python解釋器,而且它還可以執行真正的Python命令。對我來說,以些許連接代碼的代價獲得所有這些就非常了不起。
性能
當然大的問題時它是怎樣執行呢?為了分析這個,我求援於Python社團的最流行的並且不科學的基准:pystone。這是一個沒有意義的小程序,它用來 測試Python解釋器執行循環的次數,並以“每秒執行pystone的個數“這樣的結果來顯示速度。下面是我在我的機器上對各種Python解釋器測試 的結果;數值越大性能越好:
解釋器 Pystones/秒 pypy.js, href="https://developer.mozilla.org/en-US/docs/SpiderMonkey" rel="nofollow">SpiderMonkey JavaScript shell的每晚構建下運行的編譯了的pypy.js。這是強化Firefox的JavaScript引擎,而且它能夠識別和優化Emscripten生成的asm.js語法。果真,這個外加的優化實質上提高了速度。 下一個最慢的是禁止了即時編譯功能(JIT)的PyPy的本地構建。把這個版本與pypy.js相比就能對在JavaScript里運行和本地代碼運行所 花費的資源開銷有所了解,我們可以看到快了大約7倍。這甚至與在其他asm.js編譯的代碼上所呈現的只是慢兩倍的結果相差很遠。不過再說一遍,我沒有做 過任何研究或者調整性能的工作。我懷疑可能有一些相對容易實現的東西可以幫助縮短這個差距。
我的系統中較快的是本地Python解釋器CPython 2.7.4。有時可能忘記的重要的一點是:沒有即時編譯器(JIT) ,PyPy解釋器通常比標准的CPython解釋器慢一些。這是它為實現靈活性目前必須付出的代價。然而任何事情需要的不是停留- PyPy的開發者一直在尋找甚至在缺少即時編譯器(JIT的情況下加速PyPy解釋器。
毋庸置疑,這兒的速度之王是啟用即時編譯功能的PyPy本地構建。
比較pypy.js和啟用了即時編譯(JIT)的本地PyPy是很容易的,結論是兩者根本就沒有可比性。現在它們的速度差異是在兩個數量級上!不過這只是 第一次嘗試,而且沒有PyPy那樣的特別的速度JavaScript版本依然正常運行。如果我們能夠成功地把PyPy的即時編譯(JIT)功能轉換為 JavaScript,那么我們就能夠彌補回大量這樣的性能差距。的確這是一個相當大的“假設”,不過是一個有趣的可選項。
要想提前看看什么可能發生,請考慮一下PyPy倉庫里pystone的單獨的RPython版本。如果我們把它從RPython變為為本地代碼,那么它將 給出機器能力的大概上限。然而,如果我們把她從RPython編譯為JavaScript,那么它將給出啟用即時編譯的PyPy的JavaScript選 項可能的大概上限:
解釋器
Pystones/秒 native rpystone 38461538 rpystone.js, href="http://asmjs.org/" rel="nofollow">asm.js規范里明確地呼吁在代碼運行的任何階段都可以生成和連接新的asm.js模塊。由於 JavaScript具有動態特性,所以即時編譯完全得到了支持,並且完全按照規范所期望那樣運行。
然而,以asm.js方式運行的代碼禁止為自身創建新的函數。如果pypy.js解釋器需要即時編譯某些代碼,那么它將不得不通過調用外部的 JavaScript函數來跳出asm.js的快速運行通道。實際上運行生成的代碼同樣需要外部跳板以允許解釋器跳出自身的asm.js模塊去調用新的代 碼,即時編譯的代碼也需要類似的跳板以回調主解釋器。
這種asm.js內部模塊連接是Emscripten路線圖的一個試探性的內容,而且不清楚它將需要多少資源開銷。如果前后跳躍所需要的所有開銷太高,那么它就容易地受困於即時編譯代碼可能帶來的性能好處里。
在PyPy方面還有一些潛在的障礙。PyPy開發者多次試圖在低級虛擬機(LLVM)上構建自己的即時編譯系統,然而重復多次后發現他們的需求受到了太多限制。提出主要的原因之一是沒有能力動態地為生成的機器碼打補丁,不能通過JavaScript即時編譯(JIT)后台實現共享。
對我來說,如何限制仍然不清楚。如果犧牲一些效率,比如向生成的代碼里加入其它檢查和標志變量,就能夠找到限制運行的地方,那么我們也許就可以從即時編譯器知道問題所在。然而如果對代碼動態地打補丁是即時編譯操作的基礎,那么我們也許純粹是運氣不好了。
最后,有人只需要試試,然后看看結果。假若我能找到這樣的時間的話,我也計划這么做。
常常有這樣的報道:帶有即時編譯的PyPy在某些基准測試上比CPython要快6倍或者更多。而且我們已經看到了asm.js代碼比本地代碼運行要慢不到三倍。結合這兩個數據,今年剩余的時間我的崇高的、瘋狂的、良好冬季但可能徒勞的目標如下:
讓運行在spidermonkey shell里的pypy.js獲得比本地CPython解釋器更快的以每秒pystone數計量的速度。
可能嗎? 我不知道。不過找到了將是很有趣的一件事!