在今年歐洲的JSConf上Emil Bay進行了一場題爲《Hand-Crafting WebAssembly》的演講。Emil表示:「如今已經有不少關於WebAssembly(WASM)的演講。遺憾的是,大多數演講是關於如何把高級語言編譯成wasm的,他們把wasm當成一個半透明的盒子。WebAssembly是一門有趣的語言,你能夠用它寫出性能低於C的代碼」。在這此的演講中,Emil向咱們演示瞭如何寫WAT(WebAssembly的文本格式)以及當擁有大內存時,如何推理算法,如何將高級結構(如循環)轉換爲基礎指令,同時得到樂趣!Emil演示瞭如何把一些難度逐漸遞增的算法轉換成基礎指令,在沒有抽象的狀況下每個算法的實現都充滿着挑戰。即時你在工做中並無使用WASM,學習計算機的最低級指令能夠撥開抽象的迷霧,揭示計算機的神奇。在開始正文以前讓咱們先一睹大佬風采 👇javascript
「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
吐槽了那麼多,到底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,而不是經過高級程序語言編譯成WebAssembly。 WebAssembly是一種二進制格式的低級(low level)類彙編語言,官方爲了讓人類可以閱讀和編輯它,還提供了相應的文本格式(wat)。瀏覽器
從一個簡單的平方計算的函數開始咱們的第一個WebAssembly模塊:
(module
(func $square
(export "square")
(param $x i32)
(result i32)
(return (i32.mul (get_local $x)
(get_local $x)))))
複製代碼
這裏咱們定義了一個平方運算的函數square,它接受一個你i32類型的參數,返回結果也是i32。經過這個模塊咱們應該注意到如下幾點:
type.op
形式的指令調用,type表明運算結果的類型,op是要作的運算操做。如:i32.mul
表示要作乘法運算(mul),運算操做的結果的類型是i32(32位整型數字)。get_local $x
,咱們使用get_local
顯示訪問了本地變量x
。咱們來看下這個模塊是如何使用的?
將上面的「First module」保存到square.wat
文件。你也能夠從handcrafting-webassembly這個倉庫直接克隆獲取源碼。
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ make #cmake, git, make required
$ npm i -g wat2js
複製代碼
生成wasm模塊和JavaScript膠水代碼文件
$ wat2wasm square.wat #生成square.wasm文件
$ wat2js square.wat -o square.js #生成加載wasm模塊的CommonJS模塊
複製代碼
使用wasm模塊。新建example.js,添加以下代碼:
var wasm = require('./square.js');
console.log(wasm.exports.square(2)); // 4
複製代碼
經過這個簡單的WebAssembly小模塊,咱們應該已經掌握了WebAssembly文本格式一些基本語法以及如何使用它。接下來咱們來看下Emil在實際工做中寫的代碼。
下面的這段代碼定義並導出了一個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)))))))
)
複製代碼
讓咱們把難度再提高一個等級。
矢量間距離計算,其實至關於兩個數組間距離的計算。這段代碼的難度就增長了不少!這裏用到了WebAssembly的線性內存(Linear memory)和loop指令。
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-native和sodium-universal(廣告時間 啊哈~),若是你感興趣的話能夠移步到他的gayhub。(完