PyPy.js: 第一步
來自: http://python.jobbole.com/84262/
近來我在JavaScript領域花費了大量的時間。這沒有完全出乎意料-當我第一次向Mozilla申請工作的時候,我被只是半開玩笑的警告:”它們雇用所有最好的Python開發人員,然后強迫它們編寫JavaScript“。做為一門語言,我并不深愛或者憎恨它,但是做為一個平臺,JavaScript卻深深吸引著我,做為一種任何地方都可運行且廉價的通用命名環境,它逐漸地被塑造并迫使其成為一個相當完美的通用運行時環境。然而如果“ 互聯網絡是平臺 ”,那么一個落伍的Python支持者做什么呢?
當然是移植Python到JavaScript!
以前就有許多方法可以做到這些。 Skulpt 和 Brython 就是在JavaScript上對Python的重新實現,其中包括為了制作一個非常引人注目的演示而開發的交互式控制臺。 Pyjamas 可以讓你把Python應用轉換為JavaScript,這樣它們就可以在瀏覽器里運行了。還有更多這樣不同程度成功并且技術非常完善的例子。
我不想弱化像這樣的項目背后所付出的非凡的努力。不過就我個人而言,我有點擔心沉湎于重新實現這種單調的工作所帶來的風險。我非常愿意充分利用為制作非常出色的Python解釋器所做的工作和制作非常出色的JavaScript運行時環境所做的工作,然后在把兩者結合在一起的時候,盡可能少的做重復實現的工作。
最終,沿著這條思路我踏出了試探性的第一步:混合兩個令人著迷的開放源代碼項目: PyPy 和 Emscripten 。
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掛接所必須的邏輯:
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 $>
$> python ./rpython/bin/rpython ./rpython/translator/goal/targetnopstandalone.py [...lotsand lotsofcompileroutput...] $> $> ./targetnopstandalone-c debug: helloworld $>
然而為了把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 $>
$> python ./rpython/bin/rpython --backend=js ./rpython/translator/goal/targetnopstandalone.py [...lotsand lotsofcompileroutput...] $> $> node ./targetnopstandalone-js debug: helloworld $>
這就是所有要做的。如果你還有多余時間的話,那么你可以執行下面命令把整個PyPy解釋器轉換為JavaScript:
> python ./rpython/bin/rpython --backend=js --opt=2 ./pypy/goal/targetpypystandalone.py [...seriously, this will take forever...] ^C $>
> python ./rpython/bin/rpython --backend=js --opt=2 ./pypy/goal/targetpypystandalone.py [...seriously, this willtakeforever...] ^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 $>
$> nodepypy.js -c 'print "HELLO WORLD"' debug: WARNING: Librarypathnot found, usingcompiled-in sys.path. debug: WARNING: 'sys.prefix' willnot beset. debug: WARNING: Makesurethepypybinaryis keptinsideitstreeoffiles. debug: WARNING: Itis okto create a symlinkto itfromsomewhereelse. 'import site' failed HELLOWORLD $>
正如你所料,第一個版本有非常多的警告:
- 沒有即時編譯器(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數計量的速度。
可能嗎? 我不知道。不過找到了將是很有趣的一件事!
讀者中任何敢賭的人都可以