來源: https://mnt.io/2018/08/28/from-rust-to-beyond-the-asm-js-galaxy/
這篇博客文章是這一系列解釋如何將Rust發射到地球以外的許多星系的文章的一部分:
前奏,
WebAssembly 星系
ASM.js星系(當前這一集)
C 星系
PHP星系,以及
NodeJS 星系
Rust解析器將要探索的第二個星系是ASM.js。這篇文章會解釋什麼是ASM.js,怎樣編譯博客解析器到ASM.js以及如何在瀏覽器中和Javascript一起使用ASM.js. 使用ASM.js的目標是當作WebAssembly不可用的備用方案。我強烈建議你讀讀前一篇關於WebAssembly的文章,因為他們有很多共同的地方
#什麼是ASM.js,為什麼需要ASM.js
Web應用的主要語言是Javascript,任何想要運行在Web上的應用都必須編譯成Javascript,比如遊戲。但是這裡有個問題:編譯輸出文件非常重(即使是WebAssembly),這樣Javascript虛擬機將很難對這樣的代碼做優化,這就導致運行緩慢或者執行低效(可以考慮用遊戲的規模來做例子)。而且在Javascript為編譯目標的情況下,一些語言基礎設施變得毫無用處,比如eval
如果有一種新的語言可以作為編譯目標,而且也能被Javascript虛擬機運行會怎麼樣?這就是WebAssembly,但是在2013年的時候解決方案就是ASM.js:
asm.js, 是一個嚴格的Javascript子集,它能夠被用作編譯器輸出的底層的,高效的目標語言。這個子語言高效的描述>了一個沙盒虛擬機,可以適用於內存不安全的語言,像C或者C++。靜態和動態的組合校驗讓Javascript可以對有效 的asm.js代碼使用一種叫做ahead-of-time(AOT)的編譯時優化策略。
因此ASM.js程序其實只是一個普通的Javascript程序。它不是一個新的語言而是Javascript的一個子集。它可以被任何Javascript虛擬機執行。但是,有個特殊的魔法聲明use asm;,會指示虛擬機用ASM.js引擎來優化這個程序。
ASM.js通過算術運算引入了類型作為標記系統。比如,x|0表示x是一個整數,+x表示x是一個雙精度小數,fround(x)表示x是一個浮點數。下面的例子聲明了一個函數fn increment(x: u32) -> u32:
function increment(x) {
x = x | 0;
return (x + 1) | 0;
}
另一個重要的區別是ASM.js以模塊的方式來運行,以和Javascript隔離。這個模塊是一個需要3個參數的函數:
但是它仍然是Javascript。因此一個好消息是如果你的虛擬機沒有對ASM.js做優化,那麼它運行起來就是普通的Javascript程序,如果有優化,那麼你就可以得到不錯的性能提升。
這個圖片展示了3個基準測試,分別對於不同的Javascript引擎:Firefox, Firefox+asm.js, Google, 和Native。
記住ASM.js是被設計為一種編譯目標。因此一般你不需要特別關心,因為那是編譯器的活。對把C或者C++編譯到Web的一個典型的編譯和執行流程如下
Emscripten,如上圖所示,是一個在這個Web平臺演進過程中非常重要的一個項目。
它是一個用來編譯輸出asm.js和WebAssembly的工具鏈,基於LLVM之上,能夠讓C和C++程序以接近原生應用的速 度運行在Web上,而且不需要任何插件。
如果你和ASM.js或者WebAssembly打交道,遲早有一天你會看到這個名字。I will not explain deeply what ASM.js is with a lot of examples. I recommend instead to read Asm.js: The Javascript Compile Target by John Resig, or Big Web app? Compile it! by Alon Zakai. 我不會用大量的例子來深入的解釋ASM.js.我推薦兩本書:Asm.js: The Javascript Compile Target by John Resig, 或者 Big Web app? Compile it! by Alon Zakai.
我們的過程有點不一樣。我們不會直接編譯Rust代碼到ASM.js,而是先編譯為WebAssembly,然後再編譯為ASM.js。
#Rust 🚀 ASM.js
這個篇章會非常的短,應該說是最簡單的一篇。要編譯Rust到ASM.js你需要先編譯到WebAssembly(參考前一篇文章)然後再編譯WebAssembly二進位到ASM.js。
事實上真正需要ASM.js的地方是那些不支持WebAssembly的瀏覽器,比如IE。它只是我們在Web運行我們程序的一個後備方案。
下面看看這個流程:
wasm2js會是你最好的朋友,它用來編譯你的WebAssembly二進位為ASM.js模塊,它屬於Binaryen項目。下面假設我們已經有了程序的WebAssembly二進位,只需要運行下面的命令:
$ wasm2js --pedantic --output gutenberg_post_parser.asm.js gutenberg_post_parser.wasm
在這一步,gutenberg_post_parser.asm.js 有212kb。這個文件包含ECMAScript 6代碼。注意這裡因為考慮了老瀏覽器如IE,所以代碼需要一點小小的轉換來優化和精簡ASM.js模塊,我們用uglify-es工具,如下:
$
$ sed -i '' '1s/^/function GUTENBERG_POST_PARSER_ASM_MODULE() {/; s/export //' gutenberg_post_parser.asm.js
$ echo 'return { root, alloc, dealloc, memory }; }' >> gutenberg_post_parser.asm.js
$
$ uglifyjs --compress --mangle --output .temp.asm.js gutenberg_post_parser.asm.js
$ mv .temp.asm.js gutenberg_post_parser.asm.js
就像我們優化WebAssembly二進位一樣,我們也可以gzip和brotli壓縮輸出文件:
$
$ gzip --best --stdout gutenberg_post_parser.asm.js > gutenberg_post_parser.asm.js.gz
$ brotli --best --stdout --lgwin=24 gutenberg_post_parser.asm.js > gutenberg_post_parser.asm.js.br
最終我們得到了下面的文件尺寸:
.asm.js: 54kb,
.asm.js.gz: 13kb,
.asm.js.br: 11kb.
都是非常小的!
思考一下,這裡面涉及到了很多的轉換:從Rust到WebAssembly到Javascript/ASM.js。。。工具的數量相對於工作量是非常少的。這展現了一個良好設計的流水線和不同工作組的人的良好合作。
題外話:如果你在讀這篇博客,我假設你是一個開發者。既然如此,我很確定你可以花幾個小時閱讀原始碼就像欣賞大師的畫作。但是你有沒有想過把Rust編譯出來的Javascript輸出是什麼樣的?
我可以說非常的喜歡它!
#ASM.js 🚀 Javascript
輸出的gutenberg_post_parser.asm.js文件包含一個唯一的函數叫做:GUTENBERG_POST_PARSER_ASM_MODULE,它返回一個對象包含了4個私有函數。
root,語法根
alloc,來分配內存
dealloc,釋放內存
memory,內存緩衝區
如果你看過上一篇WebAssembly的文章那麼你會對這些概念感到熟悉。不要指望root會返回一個完整的AST,它只會返回一個內存指針,數據需要進一步編解碼,也需要用同樣的方式對內存進行讀寫。是的,相同的方式。因此邊界層代碼完全是一樣的。你是否還記得在WebAssembly中作為Javascript邊界的Module對象?那和GUTENBERG_POST_PARSER_ASM_MODULE函數返回的完全是一樣的。你甚至可以用這個對象替換。
所有的代碼都在這裡。它完全重用WebAssembly的Javascript邊界層代碼,只是Module有一些不一樣,也沒有加載WebAssembly二進位。結果是ASM.js邊界代碼只用了34行就寫出來了,壓縮後僅有218個字節。
#結論
我們已經看到ASM.js可以在只支持Javascript的環境中(像IE)作為WebAssembly的備用方案,並可適配環境打開或者關閉ASM.js優化。
輸出的ASM.js文件以及其邊界層代碼都非常的小。設計上,ASM.js邊界層代碼重用WebAssembly的Javascript邊界層代碼,因此同樣只有一小部分外部接口代碼需要審查和維護,這非常的有用。
我們已經在前一篇文章中看到Rust很快的速度。我們也已經在比較WebAssembly版本和純Javascript版本解析器中看到同樣速度結論。然而這個結論是否也適用於ASM.js模塊呢?其實在這種情況下ASM.js只是一個備用方案,和其他備用方案一樣通常都比較明顯的慢於目標實現。下面是一個將Rust解析器作為ASM.js模塊運行的基準測試:
testJavascript parser (ms)Rust parser as an ASM.js module (ms)speedupdemo-post.html15.3682.718× 6shortcode-shortcomings.html31.0228.004× 4redesigning-chrome-desktop.html106.41619.223× 6web-at-maximum-fps.html82.9227.197× 3early-adopting-the-future.html119.88038.321× 3pygmalian-raw-html.html349.07523.656× 15moby-dick-parsed.html2,543.75361.423× 7ASM.js模塊版本的Rust解析器比純Javascript實現平均快6倍。中位數也是6倍。這和WebAssembly版本比較還是有較大差距,但是考慮到這是個備用方案,而且平均已經快了6倍了,已經很不錯了!
因此不僅是整個工作流因為Rust而變得更加安全,而且得到的結果也比Javascript快。
在這個系列的後續文章中我們將會看到Rust會到達很多的星系,Rust越多的往後旅行,也會變得更加有趣。
謝謝閱讀!