Rust & WebAssembly 初探

blank

Rust & WebAssembly 初探

lynvv.xyz/2018/07/02/Ru
一個非常快速的概覽,文章中不涉及深度細節,深度的細節可以在代碼中找到: crypto-wasm , node-crypto

0x00

隨著2017年底,四大瀏覽器廠商全部完成對WebAssembly的初步實現,以及Webpack implementing first-class support for WebAssembly的消息公佈,越來越多的團隊在實現需求的時候將WebAssembly作為備選技術之一考慮,那麼在如今的環境(Node 8.11.3 LTS,目前階段沒有嘗試瀏覽器),和相關工具鏈( wasm-pack )下WebAssembly的使用體驗、性能到底是什麼樣的狀況呢?

0x01前置背景介紹

如果你已經對WebAssembly 已經有所了解甚至已經上手試過了,那你可以放心的跳過這一節。這一小節主要介紹一些WebAssembly 相關的非常基礎的概念

WebAssembly 簡要來說有以下三個特點:

  • 二進制格式,不同於JavaScript 代碼的文本格式
  • 標準化,與JavaScript 一樣,實現了WebAssembly 標準的引擎都可以運行WebAssembly,不管是在服務器端還是瀏覽器端
  • 快速,WebAssembly 可以充分發揮硬件的能力,以後你甚至可以在WebAssembly 中使用SIMD 或直接與GPU 交互

而WebAssembly 誕生自瀏覽器環境,自然需要與JavaScript 交互,在目前支持WebAssembly 的環境中使用WebAssembly 大概需要以下步驟:

  • 加載

因為我們需要使用WebAssembly.instantiate 來實例化一個WebAssembly 模塊,而這個方法只接受ArrayBuffer 作為第一個參數,所以只能將.wasm 文件加載為ArrayBuffer 才能將它實例化。

你可以使用fetch 加載

fetch ( url ). then ( response => response . arrayBuffer () ). then ( bytes => { // your bytes here })

或者在NodeJS 中使用fs.readFile 加載:

const bytes = fs.readFileSync('hello.wasm')
  • 實例化
WebAssembly.instantiate(bytes,imporObject)

這是一個binding 的過程。 WebAssembly.instantiate 方法會將importObject 對像傳遞給wasm ,這樣在wasm 內就可以訪問到imporObject 上的屬性和方法,而WebAssembly.instantiate(bytes, imporObject) 的返回值則會將wasm 內暴露的方法交給JavaScript調用。

constimportObject={imports:{foo:arg=>console.log(arg)// 可以在 wasm 内调用
}}WebAssembly.instantiate(bytes,importObject).then(results=>{results.instance.exports.exported_func()// exports 对象上有所有 wasm 暴露的东西
})
  • 調用

無參數或者參數類型為數字的時候,可以直接調用wasm 模塊內的方法,但是一旦涉及到其它複雜類型的參數或返回值的時候,就需要直接對內存進行操作才能正常的調用和獲取返回值了。

摘自WebAssembly系列(四)WebAssembly工作原理
如果你想在JavaScript和WebAssembly之間傳遞字符串,可以利用ArrayBuffer將其寫入內存中,這時候ArrayBuffer的索引就是整型了,可以把它傳遞給WebAssembly函數。此時,第一個字符的索引就可以當做指針來使用。

blank

目前來看這是一個非常冗長和令人費解的過程,你可以在Using_the_JavaScript_API找到更多詳細的相關描述

0x02工具鏈,Emscripten與wasm-pack

提到WebAssembly 就不得不說一下Emscripten。

WebAssembly最早是由Asm.js發展而來,Emscripten與asm.js同時誕生,最初版本的asm.js就是靠Emscripten生成。

asm.js 高性能的秘密在於引擎會直接將符合asm.js 規範的代碼編譯為彙編執行,而不是像普通的JavaScript 代碼那樣先運行在虛擬機上再由引擎逐步優化。

早期的Emscripten使用llvm將靜態語言變成LLVM IR ,然後再將LLVM IR變成asm.js。而如今它還可以將LLVM IR 編譯成wasm,目前來看它是C/C++ 到asm.js/wasm 最重要的工具。

今年稍早的時候,Rust團隊公佈了Rust 2018 Roadmap ,裡面將WebAssembly的戰略地位放在了與Network services、 Command-line appsEmbedded devices平級的地位上,並成立了專門的小組Focus在WebAssembly的生態建設上。

隨後,rustwasm 團隊便發布了wasm-pack,這是一個能快速將Rust 代碼編譯到WebAssembly 並發佈到npm 的工具。對比原始的Emscripten 工具鏈,它有三個方面的功能讓我覺得非常強大方便:

  • 無需寫巨長的編譯命令/編譯腳本,一鍵編譯
    wasm-pack init或者wasm-pack init -t nodejs即可得到需要的wasm代碼與相應的js bindings。相較而言,這是C++的代碼使用Emscripten編譯到wasm的腳本(代碼來自fdconf 2018 : webassembly在全民直播的應用):
#!/usr/bin/env bash set -x rm -rf ./build mkdir -p ./build em++ --std=c++11 -s WASM=1 -Os  --memory-init-file 0 --closure=1  -s NO_FILESYSTEM=1 -s DISABLE_EXCEPTION_CATCHING=0  -s ELIMINATE_DUPLICATE_FUNCTIONS=1 -s LEGACY_VM_SUPPORT=1 --llvm-lto 1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"  -s EXPORTED_FUNCTIONS="['_sum']"  ./sum.cpp -o ./build/index.html
  • 不需要編寫巨長無比的js wrapper
    下面是一段用C++ 編寫,使用Emscripten 編譯到wasm 之後從JavaScript 端的調用過程:
function_arrayToHeap(typedArray){constnumBytes=typedArray.length*typedArray.BYTES_PER_ELEMENT;constptr=Module._malloc(numBytes);constheapBytes=newUint8Array(Module.HEAPU8.buffer,ptr,numBytes);heapBytes.set(newUint8Array(typedArray.buffer));returnheapBytes;}function_freeArray(heapBytes){Module._free(heapBytes.byteOffset);}Module.sum=function(intArray){constheapBytes=_arrayToHeap(intArray);constret=Module.ccall("sum","number",["number","number"],[heapBytes.byteOffset,intArray.length],);_feeArray(heapBytes);returnret;}

對比來看,wasm pack 生成的包可以像調用普通JavaScript 庫一樣調用:

blank

這是wasm-pack生成出來的文件,pkg目錄是wasm-pack自動生成的文件,裡面的內容髮佈為npm package之後,使用方可以直接require('crypto-wasm')來使用。

  • 自動生成.d.ts,對前端工具鏈非常友好

wasm pack 可以將這樣的代碼:

#[wasm_bindgen]pubfnsha256(input: &str)-> String{letmuthasher=Sha256::new();hasher.input_str(input);hasher.result_str()}

生成為:

exportfunctionsha256(arg0: string):string;

無需任何額外的配置

不過在這些優點的同時,目前wasm pack 還存在一些缺陷:

  • 生成的wasm 體積太大準確來說這並不是wasm pack 工具的問題,而是Rust 語言的問題。目前一個空項目打包出來的體積大概是250kb左右,當然這對於NodeJS來說就不是什麼大問題了。
  • 無法增量編譯,編譯速度極慢空項目2min 左右才能編譯好,而且重複編譯時間不會減少。
  • 還有一些Bug
    不是指生成的代碼有Bug,而是打包過程中的一些問題。比如生成的package.json 文件中的files 字段少加了文件導致發佈到npm 之後無法直接安裝使用。

0x03性能

目前這是一個非常不嚴謹的Benchmark,結果僅供參考,如果你想修改Benchmark的過程或者進行Profile,可以在crypto-wasm的bench目錄下進行修改調試。

這次Benchmark對比了4種代碼的md5性能,它們分別是:

  • NodeJS 原生crypto
  • rust-crypto 庫的wasm 版本
  • crypto-js
  • rust-crypto Node native binding 版本

在輸入字符串為hello world!時:

➜ node bench/md5.js md5#native x 637,185 ops/sec ±1.99% ( 79 runs sampled ) md5#wasm x 194,583 ops/sec ±17.49% ( 63 runs sampled ) md5#js x 86,228 ops/sec ±11.65% ( 66 runs sampled ) md5#binding x 1,172,619 ops/sec ±4.45% ( 85 runs sampled ) Fastest is md5#binding

在輸入字符串長度為200000時:

➜ node bench/md5.js md5#native x 1,496 ops/sec ±2.24% ( 91 runs sampled ) md5#wasm x 809 ops/sec ±1.46% ( 91 runs sampled ) md5#js x 60.91 ops/sec ±1.68% ( 63 runs sampled ) md5#binding x 785 ops/sec ±0.75% ( 93 runs sampled ) Fastest is md5#native

由於實現的算法存在差異,所以需要更多維度的Benchmark 才能獲得wasm 在v8 下性能的準確訊息。

但就現實場景而言,可以看到如果一些庫在Node 下沒有Native 實現,並且有相關的Rust 實現,並且恰好這個Rust 實現沒有用到系統級的API ,那麼使用wasm pack 打包一個wasm 版本並在Node 下使用還是一個性能收益非常可觀並且成本並不算太高的選擇。

相較於native binding 而言,wasm 在CI 上的優勢是巨大的,wasm 可以在任意環境任意支持的Node 版本下編譯就可以全平台使用了。而native binding 如果安裝時build 成本太高需要使用prebuilt 發布的話,CI 簡直就是噩夢,需要所有平台所有支持的Node 版本下prebuilt,然後讓安裝的一方下載編譯好的binary 。

EOF

由於時間有限,所以目前只玩耍到這一步。

後面會帶來wasm 在瀏覽器中使用的體驗和多方面的對比(如果還有下一篇。

What do you think?

Written by marketer

blank

【譯】NodeJS事件循環Part 1

blank

🎉用Node.js開發一個Command Line Interface (CLI)