【JSConf EU 2018】WebAssembly 的手工藝術

在今年歐洲的JSConf上Emil Bay進行了一場題爲《Hand-Crafting WebAssembly》的演講。Emil表示:「如今已經有不少關於WebAssembly(WASM)的演講。遺憾的是,大多數演講是關於如何把高級語言編譯成wasm的,他們把wasm當成一個半透明的盒子。WebAssembly是一門有趣的語言,你能夠用它寫出性能低於C的代碼」。在這此的演講中,Emil向咱們演示瞭如何寫WAT(WebAssembly的文本格式)以及當擁有大內存時,如何推理算法,如何將高級結構(如循環)轉換爲基礎指令,同時得到樂趣!Emil演示瞭如何把一些難度逐漸遞增的算法轉換成基礎指令,在沒有抽象的狀況下每個算法的實現都充滿着挑戰。即時你在工做中並無使用WASM,學習計算機的最低級指令能夠撥開抽象的迷霧,揭示計算機的神奇。在開始正文以前讓咱們先一睹大佬風采 👇javascript

什麼是WebAssembly

「WebAssembly(縮寫Wasm)是運行在一個基於棧的虛擬機上的二進制指令格式。Wasm是爲了把像C/C++/Rust等高級語言編譯成便攜式的目標而設計的,能夠被部署到Web端和服務端應用」。 這是WebAssembly官網的解釋,聽起來不錯,可是今天咱們能夠忘記這些,由於咱們今天用不到這些高深的技術術語。經過「WebAssembly」這個單詞你可能猜測它運行在瀏覽器端的彙編語言。實際上,它既不是很Web,也不是很Assembly(Not very Web, not very Assembly)。java

爲何這麼說WebAssembly 「Not very Web, not very Assembly」呢?git

  • 它不能直接使用Web API。
  • WebAssembly代碼不是直接運行在物理機上的,雖然它很接近物理機,但它仍然是一個抽象出來的運行環境。
  • 不能系統調用,除非你經過JavaScript給它調用通道。
  • 不能使用新的硬件設備。例如:藍牙。
  • 沒什麼魔法,只是計算。

吐槽了那麼多,到底WebAssembly是什麼呢?github

  • 64位整型(i64) WebAssembly最讓我興奮的的是它可使用64位的整型數字,這讓咱們能夠精確的描述那些須要數字計算的事物。因爲個人工做是關於密碼學的,咱們常常須要處理256位或者512位長度的二進制數字,64位整型數字的支持對性能提高確實頗有效。web

  • 性能提高(Performance Boost) 人們一般經過把代碼轉換成WebAssembly來得到性能的提高,可是根據個人經驗一般收益不像想象的那麼大。我經過之前一些實驗得出WebAssembly相對JavaScript性能大約提高了20%至30%。由於JavaScript在一些新的JavaScript引擎(v八、SipderMonkey等)上已經運行的很快了!算法

  • 精度/可預測性(Precision/Predictable) 使用JavaScript寫代碼的時候,你一般不知道寫出來的代碼性能怎麼樣,除非你研究過底層的虛擬機。使用WebAssembly你更接近代碼的底層運行,因此代碼的表現或性能將更加可預測。express

  • Run anywhere 另外一件,讓人感到興奮的的事是WebAssembly可能在不久之後成爲惟一一個能夠跨平臺、跨端運行的語言。我已經看到有人在使用WebAssembly寫Linux內核的項目,還有人在瀏覽器里加載WebAssembly模塊。npm

WebAssembly不是什麼將來的黑科技,如今丹麥已經有超過77%的瀏覽器支持,而全球也已經有超過73%的瀏覽器支持,並且Node.js 8.0以上也支持WebAssembly,因此你如今就可使用它。數組

WebAssembly Text-format

下面咱們要手擼WebAssembly,而不是經過高級程序語言編譯成WebAssembly。 WebAssembly是一種二進制格式的低級(low level)類彙編語言,官方爲了讓人類可以閱讀和編輯它,還提供了相應的文本格式(wat)。瀏覽器

1. 平方運算

從一個簡單的平方計算的函數開始咱們的第一個WebAssembly模塊:

(module 
    (func $square
        (export "square")
        (param $x i32)
        (result i32)
        (return (i32.mul (get_local $x)
                         (get_local $x)))))
複製代碼

這裏咱們定義了一個平方運算的函數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. 安裝編譯工具wabtwat2js

    $ git clone --recursive https://github.com/WebAssembly/wabt
    $ cd wabt
    $ make  #cmake, git, make required
    $ npm i -g wat2js
    複製代碼
    • 安裝完成後將 /wabt/bin目錄添加到系統的環境。mac是添加到*/etc/paths*文件。
  3. 生成wasm模塊和JavaScript膠水代碼文件

    $ 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的函數,它接受四個參數分別是x一、y一、x二、y2,返回一個64位浮點型數字。這段代碼仍是比較好理解的,有了以前的「First Module」的經驗你應該已經知道如何使用它。一樣,你能夠在handcrafting-webassembly找到它的源碼。

(module 
    (func $square
        (export "square")
        (param $x i32)
        (result i32)
        (return (i32.mul (get_local $x)
                         (get_local $x))))

    (func $f64.distance
        (export "f64.distance")
        (param $x1 i32) (param $y1 i32)
        (param $x2 i32) (param $y2 i32)
        (result f64)
        
        (local $x.dist i32)
        (local $y.dist i32)
        
        (set_local $x.dist (i32.sub (get_local $x1)
                                    (get_local $x2)))
        
        (set_local $y.dist (i32.sub (get_local $y1)
                                    (get_local $y2)))
        
        (return (f64.sqrt (f64.convert_u/i32 (i32.add (call $square (get_local $x.dist))
                                   (call $square (get_local $y.dist)))))))
                                   
)
複製代碼

讓咱們把難度再提高一個等級。

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條件爲真的時候,繼續執行指定標籤的循環。
(module 
    (memory (export "memory") 1)

    (func $i8.distance
        (export "i8.distance")
        (param $v.ptr i32)
        (param $w.ptr i32)
        (param $len i32)
        (result f64)
        
        (local $distance.sq i32)
        (local $elm i32)
        (local $offset i32)
        
        (set_local $distance.sq (i32.const 0))
        
        (loop $continue
            ;; $elm = $w[$offset] - $v[$offset]
            (set_local $elm (i32.sub (i32.load8_u (i32.add (get_local $w.ptr)
                                                           (get_local $offset)))
                                     (i32.load8_u (i32.add (get_local $v.ptr)
                                                           (get_local $offset)))))
            ;; $distance.sq += $elm ** 2
            (set_local $distance.sq (i32.add (get_local $distance.sq)
                                             (i32.mul (get_local $elm)
                                                      (get_local $elm))))
            ;; $offset++ < $len ? continue : break
            (br_if $continue (i32.lt_u (tee_local $offset 
                                                 (i32.add (get_local $offset)
                                                          (i32.const 4))) ;; bytewidth of i32
                                       (get_local $len))))

    (return (f64.sqrt (f64.convert_u/i32 (get_local $distance.sq))))))
複製代碼

經過這個例子,咱們來看下數字在memory裏面是如何存儲的。以下圖,咱們能夠看到i8類型表示的是八個比特位(bit)整型數字,也就是一個字節(byte)。i32表示的是四個字節長度的整型數字,f64表示的是八個字節的浮點型數字。因此說memory其實就是一個字節數組。在JavaScript裏面數字只有Number類型,咱們也不須要關心數字在內存中是如何存儲的,可是在WebAssembly裏,你必須知道如何爲一個數字分配合適它的內存(定義合適的類型)。

那咱們是如何解析數組的呢?答案是經過指針(pointer)和數組的長度(length)。在這裏指針也就至關於數組的下標(index),長度也就是數組分配的內存大小。

總結

經過手擼三個難度遞增的的WebAssembly模塊,對於理解WebAssembly在內存使用和運行機制應該有所收益。可是仍是要提醒你們手擼WebAssembly並不符合它的設計初衷。演講的最後階段,Emil還介紹了本身加密算法庫sodium-nativesodium-universal(廣告時間 啊哈~),若是你感興趣的話能夠移步到他的gayhub。(完

相關文章
相關標籤/搜索