PyPy.js: 第一步

發表於2016-01-30

近來我在JavaScript領域花費了大量的時間。這沒有完全出乎意料-當我第一次向Mozilla申請工作的時候,我被只是半開玩笑的警告:”它們僱用所有最好的Python開發人員,然後強迫它們編寫JavaScript“。做為一門語言,我並不深愛或者憎恨它,但是做為一個平臺,JavaScript卻深深吸引著我,做為一種任何地方都可執行且廉價的通用命名環境,它逐漸地被塑造並迫使其成為一個相當完美的通用執行時環境。然而如果“網際網路絡是平臺”,那麼一個落伍的Python支持者做什麼呢?

當然是移植Python到JavaScript!

以前就有許多方法可以做到這些。SkulptBrython就是在JavaScript上對Python的重新實現,其中包括為了製作一個非常引人注目的演示而開發的互動式控制檯。Pyjamas可以讓你把Python應用轉換為JavaScript,這樣它們就可以在瀏覽器裡執行了。還有更多這樣不同程度成功並且技術非常完善的例子。

我不想弱化像這樣的專案背後所付出的非凡的努力。不過就我個人而言,我有點擔心沉湎於重新實現這種單調的工作所帶來的風險。我非常願意充分利用為製作非常出色的Python直譯器所做的工作和製作非常出色的JavaScript執行時環境所做的工作,然後在把兩者結合在一起的時候,儘可能少的做重複實現的工作。

最終,沿著這條思路我踏出了試探性的第一步:混合兩個令人著迷的開放原始碼專案:PyPyEmscripten

PyPy

PyPy宣稱自己是“ 另一個快速的、相容性的Python語言實現“,而且還有一個花哨的 速度測試網站來支援它的這個宣告。當然,速度確實快多了,不過真正吸引我的是它的實現細節。在構建一個新的Python直譯器的過程裡,PyPy團隊建立了用於構造動態語言直譯器的強大的通用工具套件,因而PyPy專案可以分成很大程度上相互獨立的兩個部分。

第一部分就是PyPy直譯器本身,它完全用Python編寫。說的更具體一點,它是用一個稱為Rpython的Python語言的受限制子集編寫的,它保留了完整的Python語言的許多優秀的特性,並且啟用了高效的預編譯選項。這就使得與直接用C實現直譯器,也就是實現python.org的標準直譯器相比開發工作將非常容易和靈活。

第二部分就是RPython轉換工具鏈,它提供了大量可以把RPython程式碼轉換為可執行包的令人目炫的各種方法和選項。它可以把RPython程式碼轉換為可以直接編譯的低階別的C語言程式碼,或者轉換為可在Java和.NET虛擬機器上執行的高階別的位元組碼。它還可以插入幾種不同的記憶體管理模式,執行緒實現的任何一種,以及定製最終可執行包的所用的大量的其他選項。

RPython工具鏈還包含了PyPy’s速度的祕密:通常自動地為RPython程式的熱迴圈生成及時編譯器的能力。這就是深層次元級別的魅力,而且這恰恰就是執行在網際網路上的Pyhton直譯器獲得非常好的效能所必須的東西。

因此理論上,只要我們為RPython轉換工具鏈實現生成JavaScript的後端,那麼我們就能訊速地且具有很好的相容性的移植Python到JavaScript。

Emscripten

Emscripten是“JavaScript編譯器的低階虛擬機器(LLVM)“,它用來把C或者C++程式編譯為JavaScript。通常它用來把大多數已經存在的C++應用轉換後放到網際網路上,而且它還是執行在瀏覽器裡的Epic Citadel的最新演示背後的編譯器。這是相當優美的駭客技術,而且由於近來火熱的瀏覽器JavaScript效能競爭,所以生成的程式碼可以提供完全可接受的效能。

Emscripten用來把C程式設計模型對映到JavaScript的技術最近已經在一個名叫asm.js的規範里正式化了。asm.js是允許高效的預編譯選項的JavaScirpt受限子集。在可識別asm.js程式碼的JavaScript引擎裡,Emscripten編譯的程式可以使用比本地可執行包使用的資源少兩倍的情況下都可以執行。

這兩種技術混合的可能性在理論上是很明顯的:RPython工具鏈把Python程式碼編譯成C程式碼;Emscripten編譯C程式碼為JavaScript;在你的瀏覽器使用Python把它們聚在一起。

Emscripten以前確實用來把標準的基於C的直譯器編譯為JavaScript;正是這個才使repl.it上的Python shell更強大。不過解密PyPy非常速度的想法卻十分引人注目,同時RPython構建鏈的靈活性也開啟了其他可能性。那麼會怎樣呢?

RPython的JavaScript後臺

對PyPy的偉大的工作人員和Emscripten的開發人員來說,現實中混合這兩種技術幾乎同理論上聽起來那樣容易。PyPy的RPython工具鏈有一個可以讓你很容易地插入定製的編譯器或者甚至插入一個完整的新工具鏈的擴充套件的地方。我的github分支就包含把它和Emscripten掛接所必須的邏輯:

https://github.com/rfk/pypy

Emscripten努力做到像標準的Posix構建鏈那樣執行,這樣只要求你用”emcc”替換通常所用的”gcc”呼叫。我確實需要做一點調整使它更像Posix執行環境,因此你需要用到下面的分支,直到它們與上面的分支合併為止:

https://github.com/rpk/emscripten

為了把RPython程式碼編譯為常見的可執行包,你要呼叫”rpython”轉換程式。下面是一個從PyPy原始碼倉庫中提取簡單的“你好,世界”的例子,它可以直接執行:

然而為了把RPython程式碼編譯成JavaScript,你只需要指定選項”–backend=js”。生成的JavaScript檔案可以使用如nodejs這樣的JavaScript shell的命令列來執行:

這就是所有要做的。如果你還有多餘時間的話,那麼你可以執行下面命令把整個PyPy直譯器轉換為JavaScript:

或者你只想獲取最終的結果:
pypy.js

未壓縮的情況下生成的JavaScript檔案為139M.它包括完整的Python語言直譯器,幾個非常重要的內建模組以及附加的Python標準庫中所有.py檔案的列表。如果你手邊有一個JavaScrip shell的話,你可以像下面命令列這樣傳遞這些引數JavaScript shell來執行Python命令:

正如你所料,第一個版本有非常多的警告:

  • 沒有即時編譯器(JIT)。在上面,我通過傳遞”–opt=2″選項顯式地禁止了省城即時編譯器。生成即時編譯器需要一些平臺相關的程式碼的支援,實際上我仍然沒有弄清楚它應該看起來像什麼。
  • 沒有檔案系統訪問許可權,這使得在啟動的時候就列印出除錯的告警資訊。這還需要做些對Emscripten擴充套件可插拔虛擬檔案系統的工作,在將來的某個時刻可啟用本地檔案的訪問許可權。
  • 然而,為了提供Python標準庫,它使用了繫結檔案系統的快照。這使得啟動非常非常地慢,因為在進入直譯器的主迴圈之前需要把整個快照解包到記憶體。
  • 沒有互動式控制檯。輸出執行正常,不過輸入卻並不是這樣的。我仍然不想深挖細節,不過讓一些基本的東西執行應該不是太難的。
  • 丟失了許多內建模組,因為這些內建模組需要其他C級別的依賴。比如,”hashlib”模組依賴OpenSSL。我將一個接著一個地新增這些內建模組。
  • 我肯定不會像repl.it那樣在它的上面放一個基於瀏覽器的華麗的使用者介面(UI)。

因此即便沒有這些,你也不可能立刻在瀏覽器裡執行這個。不過它是真正的Python直譯器,而且它還可以執行真正的Python命令。對我來說,以些許連線程式碼的代價獲得所有這些就非常了不起。

效能

當然大的問題時它是怎樣執行呢?為了分析這個,我求援於Python社團的最流行的並且不科學的基準:pystone。這是一個沒有意義的小程式,它用來測試Python直譯器執行迴圈的次數,並以“每秒執行pystone的個數“這樣的結果來顯示速度。下面是我在我的機器上對各種Python直譯器測試的結果;數值越大效能越好:

直譯器 Pystones/秒
pypy.js, on node 877
pypy.js, on spidermonkey 7427
native pypy, no JIT 53418
native cPython 128205
native pypy, with JIT 781250

到目前為止,最慢的是執行在碰巧安裝的穩定版本的nodejs上的編譯了的pypy.js。實質上這是JavaScript的基本效能,因為這個版本的node沒有對有Emscripten生成的asm.js風格的程式碼做任何特別特殊的處理。如果我構建目前的開發版本,那麼它可能執行地會更快些。

下一個最慢的就是在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, on spidermonkey 13531802

 

比較上面的pystone執行後的結果,數字高的驚人。這麼高以致於我懷疑這些數值是否完全精確,並且由於pystone的RPython版本和標準版本的某些不同而出錯。然而實際上根本就沒有不同之處。

這兒最有趣的事情是比較這兩個版本的效能。JavaScript版本比本地的編譯版本慢3倍多,而與使用全功能的直譯器相比則是更小的差距。是否啟用即時編譯的pypy.js執行流行的迴圈僅僅比本地直譯器滿3倍嗎?這是一個有趣的發現。

能不能即時編譯?

眼下留給我一個急需解決的問題是:JavaScript平臺是否強大到足以支援PyPy的即時編譯功能?坦率地講,我不知道!不過更加深入地探究RPython即時生成器的細節並且弄清楚由Emscripten和asm.js構建的JavaScript後臺看起來像什麼是我正在進行的工作。

從JavaScript角度來看,有非常積極一點: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數計量的速度。

可能嗎? 我不知道。不過找到了將是很有趣的一件事!

讀者中任何敢賭的人都可以
直接向Brendan Eich詢問

相關文章