天天看點

【JSConf EU 2018】WebAssembly 的手工藝術

在今年歐洲的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是要做的運算操作。如:

    i32.mul

    表示要做乘法運算(mul),運算操作的結果的類型是i32(32位整型數字)。
  • 顯示通路,當要使用一個變量時,我們需要顯示通路。如:

    get_local $x

    ,我們使用

    get_local

    顯示通路了本地變量

    x

我們來看下這個子產品是如何使用的?

  1. 将上面的“First module”儲存到

    square.wat

    檔案。你也可以從 handcrafting-webassembly 這個倉庫直接克隆擷取源碼。
  2. 安裝編譯工具 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

    指令用來定義循環代碼塊。緊跟在loop指令後面需要定義一個标簽,在這裡我們定義的是“continue”。WebAssembly的循環和JavaScript有些不同。在JavaScript的循環裡有continue和break兩個分支。WebAssembly的循環比較像do-while循環,但它隻有一個條件分支,當

    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

 如需轉載請聯系原作者