在今年歐洲的JSConf上Emil Bay進行了一場題為《Hand-Crafting WebAssembly》的演講。Emil表示:“現在已經有很多關于WebAssembly(WASM)的演講。遺憾的是,大多數演講是關于如何把進階語言編譯成wasm的,他們把wasm當成一個半透明的盒子。WebAssembly是一門有趣的語言,你可以用它寫出性能低于C的代碼”。在這此的演講中,Emil向我們示範了如何寫WAT(WebAssembly的文本格式)以及當擁有大記憶體時,如何推理算法,如何将進階結構(如循環)轉換為基礎指令,同時獲得樂趣!Emil示範了如何把一些難度逐漸遞增的算法轉換成基礎指令,在沒有抽象的情況下每一個算法的實作都充滿着挑戰。即時你在工作中并沒有使用WASM,學習計算機的最低級指令可以撥開抽象的迷霧,揭示計算機的神奇。在開始正文之前讓我們先一睹大佬風采
什麼是WebAssembly
“WebAssembly(縮寫Wasm)是運作在一個基于棧的虛拟機上的二進制指令格式。Wasm是為了把像C/C++/Rust等進階語言編譯成便攜式的目标而設計的,可以被部署到Web端和服務端應用”。 這是WebAssembly官網的解釋,聽起來不錯,但是今天我們可以忘記這些,因為我們今天用不到這些高深的技術術語。通過“WebAssembly”這個單詞你可能猜想它運作在浏覽器端的彙編語言。實際上,它既不是很Web,也不是很Assembly(Not very Web, not very Assembly)。
為什麼這麼說WebAssembly “Not very Web, not very Assembly”呢?
- 它不能直接使用Web API。
- WebAssembly代碼不是直接運作在實體機上的,雖然它很接近實體機,但它仍然是一個抽象出來的運作環境。
- 不能系統調用,除非你通過JavaScript給它調用通道。
- 不能使用新的硬體裝置。例如:藍牙。
- 沒什麼魔法,隻是計算。
吐槽了那麼多,到底WebAssembly是什麼呢?
-
64位整型(i64)
WebAssembly最讓我興奮的的是它可以使用64位的整型數字,這讓我們可以精确的描述那些需要數字計算的事物。由于我的工作是關于密碼學的,我們經常需要處理256位或者512位長度的二進制數字,64位整型數字的支援對性能提升确實很有效。
-
性能提升(Performance Boost)
人們通常通過把代碼轉換成WebAssembly來獲得性能的提升,但是根據我的經驗通常收益不像想象的那麼大。我通過以前一些實驗得出WebAssembly相對JavaScript性能大約提升了20%至30%。因為JavaScript在一些新的JavaScript引擎(v8、SipderMonkey等)上已經運作的很快了!
-
精度/可預測性(Precision/Predictable)
使用JavaScript寫代碼的時候,你通常不知道寫出來的代碼性能怎麼樣,除非你研究過底層的虛拟機。使用WebAssembly你更接近代碼的底層運作,是以代碼的表現或性能将更加可預測。
-
Run anywhere
另一件,讓人感到興奮的的事是WebAssembly可能在不久以後成為唯一一個可以跨平台、跨端運作的語言。我已經看到有人在使用WebAssembly寫Linux核心的項目,還有人在浏覽器裡加載WebAssembly子產品。
WebAssembly不是什麼未來的黑科技,現在丹麥已經有超過77%的浏覽器支援,而全球也已經有超過73%的浏覽器支援,而且Node.js 8.0以上也支援WebAssembly,是以你現在就可以使用它。
WebAssembly Text-format
下面我們要手撸WebAssembly,而不是通過進階程式語言編譯成WebAssembly。 WebAssembly是一種二進制格式的低級(low level)類彙編語言,官方為了讓人類能夠閱讀和編輯它,還提供了相應的文本格式(wat)。
1. 平方運算
從一個簡單的平方計算的函數開始我們的第一個WebAssembly子產品:
這裡我們定義了一個平方運算的函數square,它接受一個你i32類型的參數,傳回結果也是i32。通過這個子產品我們應該注意到以下幾點:
- wat文本采用的是S-expressions的文法(類似LISP)。
- 子產品是WebAssembly的基本機關,這點和ES6的子產品很像。
- 标簽(參數名、變量名和函數名)使用 $ 字首聲明。
- 明确的類型,參數、變量、函數傳回都有類型聲明。
- 運算操作是通過
形式的指令調用,type代表運算結果的類型,op是要做的運算操作。如:type.op
表示要做乘法運算(mul),運算操作的結果的類型是i32(32位整型數字)。i32.mul
- 顯示通路,當要使用一個變量時,我們需要顯示通路。如:
,我們使用get_local $x
顯示通路了本地變量get_local
。x
我們來看下這個子產品是如何使用的?
- 将上面的“First module”儲存到
檔案。你也可以從 handcrafting-webassembly 這個倉庫直接克隆擷取源碼。square.wat
- 安裝編譯工具 wabt 和 wat2js
$ wat2wasm square.wat #生成square.wasm檔案
$ wat2js square.wat -o square.js #生成加載wasm子產品的CommonJS子產品
4.使用wasm子產品。建立example.js,添加如下代碼:
var wasm = require('./square.js');
console.log(wasm.exports.square(2)); // 4
通過這個簡單的WebAssembly小子產品,我們應該已經掌握了WebAssembly文本格式一些基本文法以及如何使用它。接下來我們來看下Emil在實際工作中寫的代碼。
2. 計算兩點之間距離
下面的這段代碼定義并導出了一個
f64.distance
的函數,它接受四個參數分别是x1、y1、x2、y2,傳回一個64位浮點型數字。這段代碼還是比較好了解的,有了之前的“First Module”的經驗你應該已經知道如何使用它。同樣,你可以在
找到它的源碼。
讓我們把難度再提升一個等級。
3. 計算矢量間的距離
矢量間距離計算,其實相當于兩個數組間距離的計算。這段代碼的難度就增加了很多!這裡用到了WebAssembly的線性記憶體(Linear memory)和loop指令。
- memory是WebAssembly的一個重要的概念,它是用來實作JavaScript和WebAssebly子產品間通信的,本質上就是一個大的共享數組。下面的子產品中,我們建立并導出了一頁(64KiB)大小的momery執行個體。導出的memory執行個體是提供給JavaScript使用。通過JavaScript把外部數組的資料存到memory,然後我們可以WebAssebly子產品裡通路它。
-
指令用來定義循環代碼塊。緊跟在loop指令後面需要定義一個标簽,在這裡我們定義的是“continue”。WebAssembly的循環和JavaScript有些不同。在JavaScript的循環裡有continue和break兩個分支。WebAssembly的循環比較像do-while循環,但它隻有一個條件分支,當loop
條件為真的時候,繼續執行指定标簽的循環。br_if
通過這個例子,我們來看下數字在memory裡面是如何存儲的。如下圖,我們可以看到
i8
類型表示的是八個比特位(bit)整型數字,也就是一個位元組(byte)。
i32
表示的是四個位元組長度的整型數字,
f64
表示的是八個位元組的浮點型數字。是以說memory其實就是一個位元組數組。在JavaScript裡面數字隻有
Number
類型,我們也不需要關心數字在記憶體中是如何存儲的,但是在WebAssembly裡,你必須知道如何為一個數字配置設定合适它的記憶體(定義合适的類型)。
那我們是如何解析數組的呢?答案是通過指針(pointer)和數組的長度(length)。在這裡指針也就相當于數組的下标(index),長度也就是數組配置設定的記憶體大小。
總結
通過手撸三個難度遞增的的WebAssembly子產品,對于了解WebAssembly在記憶體使用和運作機制應該有所收益。但是還是要提醒大家手撸WebAssembly并不符合它的設計初衷。演講的最後階段,Emil還介紹了自己加密算法庫
sodium-native sodium-universal(廣告時間 啊哈~),如果你感興趣的話可以移步到他的gayhub。(完
原文釋出時間為:2018年06月29日
本文作者:leyayun
本文來源:
掘金 https://juejin.im/post/5b38d27451882574d87aa5d5如需轉載請聯系原作者