使用 Rust + WebAssembly 編寫 crc32

背景

WebAssembly 在最近幾年裏能夠說是如火如荼了。從基於 LLVM 的 Emscripten ,到嘗試打造全流程工具鏈的 binaryen ,再到 Rust 社區出現的wasm-bindgen……如今 webpack 4 已經內置了wasm的引入,甚至連 Go 社區也不甘落後地推出了相關的計劃javascript

做爲一個普通的前端開發者,我雖然一直在關注它的發展,但始終沒有直接操刀使用的機會。直到最近,咱們想使用 crc32 算法作一些字符串校驗時,我想看看 WebAssembly 是否可以在這項計算任務上,比原生 JavaScript 更具備性能優點。html

有關 crc32

crc32 算法是一個專門用於 校驗數據 是否被意外篡改的算法。它在計算量上比md5 、 sha 這類密碼學信息摘要算法要小不少,但修改任何一個字節,都會引發校驗和發生變化。crc32 並非密碼學安全的,構造兩組校驗和相同的數據並不困難。所以,crc32 適合用在乎外篡改的檢查上,而不適合用在對抗人工篡改的環境下。前端

在原理上, crc32 能夠被看做是使用數據對某個選定的數字(Polynomial,常被縮寫爲「Poly」,實際是一個生成數字的多項式簡寫形式),進行某種形式的除法。除法產生的餘數,就是校驗和。具體的算法原理略微複雜一些,你們能夠參考這篇《無痛理解CRC》java

不一樣的數字會對算法有很強的影響。在計算機領域,有好幾個不一樣的數字在不一樣的領域採用。gzip 用的 crc32 的數字,就和 ext4 文件系統用的不一樣。jquery

歷史上, crc32 的算法也被改進過屢次。從最簡單的逐位計算,到採用查找表進行優化,再到使用多張查找表優化,其性能被提高了數百倍之多。關於這點,你們能夠在 Fast CRC32 上查看詳情。對於大部分場景,咱們追求性能而不是代碼體積,所以儘量利用查找表,可以讓算法發揮最強的性能。webpack

要想對比 JS 版和 Rust 版 crc32 的性能差距,首先要排除掉算法實現不一樣帶來的影響。所以,下面我在進行性能對比時所採用的 crc32 算法,都是我本身參考第三方代碼來寫的,並不直接採用現成的包。git

不過因爲 crc32 的實現版本太多,這裏只挑取其中性能較好同時查找表體積適中的 Slicing-by-8 實現來寫。github

用 JavaScript 寫一個 crc32

如今咱們新建一個crc32.js文件,存放我寫的 crc32 。這種 crc32 的實現須要進行兩個步驟,第一個步驟是生成查找表:web

// crc32.js
const POLY = 0xedb88320;
const TABLE = makeTable(POLY);
const TABLE8 = (function () {
  const tab = Array(8);
  for (let i = 0; i < 8; i++) {
    tab[i] = new Uint32Array(256);
  }
  tab[0] = makeTable(POLY);
  for (let i = 0; i <= 0xFF; i++) {
    tab[1][i] = (tab[0][i] >>> 8) ^ tab[0][tab[0][i] & 0xFF];
    tab[2][i] = (tab[1][i] >>> 8) ^ tab[0][tab[1][i] & 0xFF];
    tab[3][i] = (tab[2][i] >>> 8) ^ tab[0][tab[2][i] & 0xFF];
    tab[4][i] = (tab[3][i] >>> 8) ^ tab[0][tab[3][i] & 0xFF];
    tab[5][i] = (tab[4][i] >>> 8) ^ tab[0][tab[4][i] & 0xFF];
    tab[6][i] = (tab[5][i] >>> 8) ^ tab[0][tab[5][i] & 0xFF];
    tab[7][i] = (tab[6][i] >>> 8) ^ tab[0][tab[6][i] & 0xFF];
  }
  return tab;
})();

function makeTable(poly) {
  const tab = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    let crc = i;
    for (let j = 0; j < 8; j++) {
      if (crc & 1 === 1) {
        crc = (crc >>> 1) ^ poly;
      } else {
        crc >>>= 1;
      }
      tab[i] = crc;
    }
  }
  return tab;
}
複製代碼

這個步驟我放在模塊全局了,由於查找表只須要生成一次,後面實際進行 crc32 的計算時,只讀就能夠了。算法

第二個步驟就是 crc32 自己的計算:

// 續crc32.js
// 讀取和拼裝32位整數
function readU32(buf, offset) {
  return buf[0 + offset] + ((buf[1 + offset]) << 8) + ((buf[2 + offset]) << 16) + ((buf[3 + offset]) << 24);
}

// 實際計算
function crc32(buf) {
  let crc = ~0;
  let leftLength = buf.byteLength;
  let bufPos = 0;
  while (leftLength >= 8) {
    crc ^= readU32(buf, bufPos);
    crc = TABLE8[0][buf[7 + bufPos]] ^
    TABLE8[1][buf[6 + bufPos]] ^
    TABLE8[2][buf[5 + bufPos]] ^
    TABLE8[3][buf[4 + bufPos]] ^
    TABLE8[4][(crc >>> 24) & 0xFF] ^
    TABLE8[5][(crc >>> 16) & 0xFF] ^
    TABLE8[6][(crc >>> 8) & 0xFF] ^
    TABLE8[7][crc & 0xFF];
    bufPos += 8;
    leftLength -= 8;
  }
  for (let byte = 0; byte < leftLength; byte++) {
    crc = TABLE[(crc & 0xFF) ^ buf[byte + bufPos]] ^ (crc >>> 8);
  }
  return ~crc;
}

module.exports = crc32;
複製代碼

爲方便將來對比,我將這個函數導入並從新命名,而後搭建一個對比測試的環境:

// index.js
const Benchmark = require('benchmark');
const crc32ByJs = require('./crc32');
// 導入測試文本數據
const testSource = fs.readFileSync('./fixture/jquery.js.txt', 'utf-8');
const text = testSource;
// 爲了屏蔽掉編碼帶來的性能影響,我預先就將字符串編碼
const textInU8 = stringToU8(text);

// 輔助工具函數,幫咱們把字符串編碼 的二進制數據
function stringToU8(text) {
  return Buffer.from(text, 'utf8');
}
複製代碼

注意,這裏雖然使用了 UTF-8 ,但其實也能夠選擇其餘的編碼,好比 UTF-16 或者 UTF-32,只不過 UTF-8 的支持更加普遍一些,另外沒必要關心字節序,也更方便於解碼。

如今咱們能夠開始搞 WebAssembly 版的 crc32ByWasm了。

WebAssembly 與 Rust

WebAssembly 自己是很是相似機器碼的一種語言,它緊湊且使用二進制來表達,所以在體積上自然有優點。但要讓開發者去寫機器碼,開發成本會很是高,所以伴隨着 WebAsssembly 出現的還有相應的人類可讀的文本描述—— S 表達式描述

(module
  (type $type0 (func (param i32 i32) (result i32)))
  (table 0 anyfunc)
  (memory 1)
  (export "memory" memory)
  (export "add" $func0)
  (func $func0 (param $var0 i32) (param $var1 i32) (result i32)
    get_local $var1
    get_local $var0
    i32.add
  )
)
複製代碼

S表達式已經比機器碼可讀性強不少了,但咱們能使用的依然是一些很是底層的操做,比較相似彙編語言。所以,目前更常見的玩法,是將其餘編程語言編譯到 WebAssembly,而不是直接去寫 WebAssembly 或者 S 表達式。

Rust 社區在這方面目前進展比較不錯,有專門的工做小組來支持這件事。我雖然以前沒有太多 Rust 經驗,但此次很是想利用社區的工做成果,以避開其餘語言生成 WebAssembly 的各類不便。

Rust與WebAssembly

打造 Rust 工具鏈

Rust 社區和 JavaScript 社區有一些類似,你們都是樂於在工程化上投入精力,並致力於提高開發溫馨度的羣體。搭建一個 Rust 開發環境其實很是簡單,總共只須要3步:

  1. 下載並安裝 rustup。這一步和安裝 nvm 差很少。
  2. 使用 rustup 來安裝和使用 nightly 版的 rust。這一步至關於使用 nvm 安裝具體的 Node.js 版本
  3. 繼續使用 rustup,下載安裝名爲 wasm32-unknown-unknown 的編譯目標。這一步是 rust 獨有的了,不過實際上任何能交叉編譯的編譯器,都要來這麼一遍。

這裏稍微說一下什麼叫作「交叉編譯」。

正常來說,若是我在 Linux x86 的系統裏安裝一套 C++ 編譯器,那麼當我使用這套編譯器生成可執行程序的時候,它生成的就是本機能用的程序。那若是我有一臺 Windows 的機器,卻沒有在其中安裝任何編譯器,該怎麼辦呢?這時,若是有一套 C++ 編譯器能在 Linux x86 上運行,但產生的代碼倒是執行在 Windows 上的,這套編譯器就是交叉編譯工具了。相對應的,這個過程就叫作交叉編譯。

如以前所說, WebAssembly 是一種機器碼,那麼用 Rust 編譯器(原本生成的是macOS或者Linux x86的可執行程序)生成它,天然就是一種交叉編譯了。

這個過程整理成腳本就是以下的樣子了:

# 執行完這句話之後,和安好nvm同樣,要在命令行裏引入一下 rustup
curl https://sh.rustup.rs -sSf | sh
rustup toolchain install nightly # 安裝 nightly 版 rust
rustup target add wasm32-unknown-unknown # 安裝交叉編譯目標
複製代碼

注意,不一樣的平臺上的Rust安裝過程可能略有差別,屆時須要根據具體狀況來作調整。明確本身所用的 Rust 版本很是重要,由於 Rust 對 WebAssembly 的支持還在早期階段,一些工程化的代碼隨時可能發生變化。在寫這篇文章時,我所用的 Rust 版本爲 rustc 1.28.0-nightly (2a1c4eec4 2018-06-25)。

建立一個 Rust 項目

安裝好 Rust 以後,會自帶一個名爲 cargo 的命令行。cargo 是 Rust 社區的包管理命令行工具,比較相似於 Node.js 社區的 npm 。建立 Rust 項目能夠直接使用 cargo 進行:

cargo new crc32-example
複製代碼

這樣咱們就能夠在當前目錄下建立一個新目錄 crc32-example,並在其中初始化好了咱們的代碼。cargo 默認會新建兩個文件,分別是 Cargo.tomllib.rs(具體代碼可參見文末的源碼),他們的做用分別是:

  • Cargo.toml至關因而 Rust 社區的 package.json,用於存放依賴描述和一些項目元信息。
  • lib.rs是代碼的入口文件,之後咱們寫的 Rust 代碼就會放在其中。

下面咱們會詳細說說 WebAssembly 的調用。

Node.js 調用 WebAssembly

Node.js 不一樣的版本對 WebAssembly 支持各不相同,在我本身的測試中發現,Node.js 8.x的支持就算是比較穩定了,所以後面我都會用 Node.js 8.x 來寫。

WebAssembly 在 JavaScript 中如何調用的文章在網上比較多了,你們能夠本身搜索參考一下,這裏我只列出一些核心,不作具體的介紹了。

WebAssembly 在 JavaScript 當中能夠被看做是一種特殊「模塊」 ,這個模塊對外導出若干函數,同時也能接受 JavaScript 向其中導入函數。因爲 JavaScript 本身的內存管理是經過垃圾回收器來自動作的,而其餘一些靜態語言一般是開發者手動管理內存,WebAssembly 當中所用的內存,須要從普通的 JavaScript 內存中區分開來,單獨開闢和管理。

在使用 WebAssembly 時,首先要對其進行初始化。初始化的時候,JavaScript 引擎會校驗 WebAssembly 的合法性,並將單獨開闢內存、導入函數,和模塊進行關聯。 這個過程變成代碼的話,就是以下的樣子:

// 續index.js
const wasmFile = fs.readFileSync('./target/wasm32-unknown-unknown/release/wasm_crc32_example.wasm');

const wasmValues = await WebAssembly.instantiate(wasmFile, {
  env: {
    memoryBase: 0,
    tableBase: 0,
    // 單獨開闢的內存
    memory: new WebAssembly.Memory({
      initial: 0,
      maximum: 65536,
    }),
    table: new WebAssembly.Table({
      initial: 0,
      maximum: 0,
      element: 'anyfunc',
    }),
    // 導入函數,若是要在 Rust 當中使用任何 JavaScript 函數,都要像這樣導入
    logInt: (num) => {
      console.log('logInt: ', num);
    },
  },
});
複製代碼

WebAssembly.instantiate 將返回一個 Promise 對象,對象內部咱們關心的是instance 屬性,它就是初始化後可用的 WebAssembly 對象了:

// 續index.js
const wasmInstance = wasmValues.instance;

const {
  // 將 WebAssembly 導出的函數 crc32 重命名爲 crc32ByWasm
  // 由於咱們已經有一個 JavaScript 的實現,以防混淆
  crc32: crc32ByWasm,
} = wasmInstance.exports;

const text = testSource;
const checksum = crc32ByWasm(text);
複製代碼

上面的代碼嘗試使用 WebAssembly 導出的函數,來計測試文本的校驗和。

然而,這種代碼實際上是行不通的。最大的問題在於,WebAssembly 是沒有真正的字符串類型的

WebAssembly 在當前的設計中,可以使用類型其實只有各類類型的數字,從8位整數到64位整數都有。但這裏面沒有布爾值,也沒有字符串等相對比較有爭議的類型。

所以,在 JavaScript 和 WebAssembly 之間傳遞字符串,要靠開闢出的內存來進行輔助傳遞。

共享內存塊

有 C 編程基礎的同窗可能這裏會比較容易理解,這個字符串的傳遞,其實就是把 JavaScript 中的字符串,編碼爲 UTF-8 ,而後逐字節複製到內存當中:

// 續index.js
function copyToMemory(textInU8, memory, offset) {
  const byteLength = textInU8.byteLength;

  const view = new Uint8Array(memory.buffer);
  for (let i = 0; i < byteLength; i++) {
    view[i + offset] = textInU8[i];
  }
  return byteLength;
}
複製代碼

實際在使用內存塊時,每每須要更加精細的內存管理,以便同一塊內存塊能夠儘量地屢次使用而又不破壞先前的數據。

上面的memory來自wasmInstance.exports,因此咱們的代碼須要稍微調整一下了:

// 續index.js
const {
  // 注意這裏須要導出的 memory
  memory,
	crc32: crc32ByWasm,
} = wasmInstance.exports;

const text = "testSource";
const textInU8 = stringToU8(text);
const offset = 10 * 1024;
const byteLength = copyToMemory(textInU8, memory, offset);
crc32ByWasm(offset, byteLength);
複製代碼

注意crc32ByWasm的第一個參數,這個參數所表明的含義是字符串數據在內存塊的偏移量。

在進行測試時,我發現內存塊的開頭有時會出現其餘數據,所以這裏我偏移了 10KB ,以防和這些數據發生衝突。我沒有深究,但我以爲這些數據頗有多是 WebAssembly 機器碼附帶的數據,好比查找表。

用 Rust 寫一個 crc32

Rust 社區有本身的包管理工具,同時也有本身的依賴託管網站,我在其中找到了crc32這個模塊。但如同前面所說,咱們但願此次作性能測試的時候,可以排除算法實現差別帶來的影響,所以 Rust 版的 crc32 我沒有直接使用它,而是本身從rust-snappy 裏複製出來了一份類似的實現,而後稍微作了些改動。

算法的實現和 JavaScript 差很少,所以不詳細貼在這裏了,惟獨這個實現的導出,可能你們會有些不解,所以我下面稍做一些解釋,剩下的你們看文末的源碼就能夠了:

// no_mangle 標記會告知編譯器,crc32 這個函數的名字和參數不要進行改動
// 由於咱們要保持這個函數的接口對 WebAssembly 可用
#[no_mangle]
pub extern fn crc32(ptr: *mut u8, length: u32) -> u32 {
  // std::slice::from_raw_parts 對於編譯器來講會產生不可知的後果,這裏須要 unsafe 來去除編譯器的報錯
	unsafe {
		// 將咱們傳遞進來的偏移量和長度,轉化爲 Rust 當中的數組類型
		let buf : &[u8] = std::slice::from_raw_parts(ptr, length as usize);
    return crc32_internal(buf);
  }
}
複製代碼

每一行的含義基本都寫在註釋裏了,這裏面惟一比較難理解概念,大概是unsafe 了。

Rust 這門語言的設計哲學當中包含一項「內存安全」。也就是說,使用 Rust 寫出的代碼 ,都應該不會引起內存使用上帶來的問題。Rust 作到這一點,靠的是編譯器的靜態分析,這就要求全部內存使用,在編譯時就肯定下來。可是在咱們的代碼當中,咱們須要使用 WebAssembly 當中的內存塊,而內存塊的實際狀況,是在運行時才真正可以肯定的。

這種矛盾就體如今咱們須要 Rust 信任咱們傳遞過來的「偏移量」上。所以這段代碼須要被標記爲 unsafe,以便讓編譯器充分地信任咱們所寫的代碼。

Benchmark 與性能調優

好了,如今 WebAssembly 版的代碼和 JavaScript 版的代碼都有了,我想看看他們誰跑的更快一些,因此弄了個簡單的 benchmark :

// 續index.js
const suite = new Benchmark.Suite;
const offset = 10 * 1024;
const byteLength = copyToMemory(textInU8, memory, offset);

suite
  .add('crc32ByWasm', function () {
    crc32ByWasm(offset, byteLength);
  })
  .add('crc32ByJs', function () {
    crc32ByJs(textInU8);
  })
  .on('cycle', function (event) {
    console.log(String(event.target));
  })
  .on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({
  	'async': true
});
複製代碼

是騾子是馬拉出來溜溜了!

crc32ByJs x 22,444 ops/sec ±1.20% (83 runs sampled)
crc32ByWasm x 37,590 ops/sec ±0.90% (89 runs sampled)
Fastest is crc32ByWasm
複製代碼

太好了,性能有67%的提高。如今咱們能夠證實 WebAssembly 版的 crc32 確實比 JavaScript 版的快了。

但這裏還有一個問題被忽略了,那就是若是咱們要使用 WebAssembly 版的 crc32 ,咱們就不得不將其複製到 WebAssembly 的內存塊中;而若是咱們使用 JavaScript 版本的就沒必要這樣。因而,我又從新作了一次性能測試,此次我在測試中充分考慮了內存複製:

crc32ByJs x 21,383 ops/sec ±2.36% (80 runs sampled)
crc32ByWasm x 34,938 ops/sec ±0.86% (84 runs sampled)
crc32ByWasm (copy) x 16,957 ops/sec ±1.74% (79 runs sampled)
Fastest is crc32ByWasm
複製代碼

能夠看出,增長了內存複製和編碼以後,WebAssembly 版本的性能跌落很是明顯,和 JavaScript 相比已經沒有優點了。不過這是 Node.js 當中的狀況,瀏覽器中會不會有什麼不一樣呢?因而我嘗試了一下在瀏覽器中進行測試。

在瀏覽器中嘗試 WebAssembly

除了IE,其餘比較先進的瀏覽器都已經支持了 WebAssembly。這裏我就使用 Chrome 67 來進嘗試。

這裏,瀏覽器和 Node.js 環境差異不大,只是字符串的編碼,沒有 Buffer 幫咱們去作了,咱們須要調用 API 來進行:

function stringToU8(text) {
  const encoder = new TextEncoder();
  return encoder.encode(text);
}
複製代碼

Webpack 4雖然已經支持了 WebAssembly ,但爲了可以自定義初始化 WebAssembly 模塊,我仍是採用了單獨的arraybuffer-loader 來加載 WebAssembly 模塊。具體的配置和代碼能夠參考個人源碼。

測試結果是,JavaScript 版的 crc32 更慢了, JavaScript 版的實現雖然看起來比帶內存複製的 WebAssembly 版更快,但優點不明顯:

crc32ByJs x 10,801 ops/sec ±1.28% (52 runs sampled)
crc32ByWasm x 28,142 ops/sec ±1.13% (51 runs sampled)
crc32ByWasm (copy) x 11,604 ops/sec ±1.16% (54 runs sampled)
Fastest is crc32ByWasm
複製代碼

考慮到實際在業務中使用時,幾乎老是要進行內存複製的,WebAssembly 版本的 crc32 即便在計算上有優點,也會被內存問題給掩蓋,實用性大打折扣。

在某些狀況下 Webpack 4 自帶的 uglify 會產出帶有語法錯誤的文件,所以在實際測試時我關掉了 uglify 。

優化尺寸

執行性能上的對比暫時告一段落了,但咱們前端工程師除了關注執行性能外,還關注模塊的實際體積。

在 webpack 打包時,我刻意留意了 WebAssembly 相關文件的打包,結果使人大跌眼鏡:

webpack v4.12.0

6d1b9c1ec10ef7b04017
  size     name  module                                                           status
  489 B    0     (webpack)/buildin/global.js                                      built
  32.4 kB  1     ./fixture/jquery.js.txt                                          built
  879 kB   2     ./target/wasm32-unknown-unknown/release/wasm_crc32_example.wasm  built
  1.8 kB   8     ./crc32.js                                                       built
  497 B    10    (webpack)/buildin/module.js                                      built
  2.25 kB  12    ./browser.js                                                     built

  size     name  asset                                                            status
  1.03 MB  main  app.js                                                           emitted

  Δt 3837ms (7 modules hidden)
複製代碼

結果中, wasm_crc32_example.wasm 佔據了使人驚訝的 879 kB。而 crc32.js 只佔 1.8 kB

說好的更緊湊的二進制呢!區區一個 crc32 怎麼會這麼大呢?順着社區的指引,我開始使用 wasm-gc 來嘗試優化體積。

使用以後的狀況:

webpack v4.12.0

0f45cfd553d632ac59ce
  size     name  module                                                           status
  489 B    0     (webpack)/buildin/global.js                                      built
  32.4 kB  1     ./fixture/jquery.js.txt                                          built
  313 kB   2     ./target/wasm32-unknown-unknown/release/wasm_crc32_example.wasm  built
  1.8 kB   8     ./crc32.js                                                       built
  497 B    10    (webpack)/buildin/module.js                                      built
  2.25 kB  12    ./browser.js                                                     built

  size     name  asset                                                            status
  459 kB   main  app.js                                                           emitted

  Δt 3376ms (7 modules hidden)
複製代碼

wasm_crc32_example.wasm 的體積被縮減到了 313 kB。但我仍是以爲不夠滿意——我明明也就寫了幾十行代碼而已。爲此我藉助 twiggy 檢查了生成的 wasm 文件包含什麼:

Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼──────────────────────────────────────────────────────────────────────────────
        227783 ┊    34.62% ┊ "function names" subsection
        87802 ┊    13.34% ┊ data[0]
        21853 ┊     3.32% ┊ data[1]
          4161 ┊     0.63% ┊ core::num::flt2dec::strategy::dragon::format_shortest::hf5755820aea88984
          3471 ┊     0.53% ┊ core::num::flt2dec::strategy::dragon::format_exact::hc11617164ea3324a
          3466 ┊     0.53% ┊ dlmalloc::dlmalloc::Dlmalloc::malloc::hc22818825fdee93b
          2325 ┊     0.35% ┊ core::num::flt2dec::strategy::grisu::format_shortest_opt::he434a538cbbb5c09
          2247 ┊     0.34% ┊ <std::net::ip::Ipv6Addr as core::fmt::Display>::fmt::hee517e812c10fa59
複製代碼

注意,分析結果已通過精簡。實際分析結果很是冗長,這裏只截取了最有助於判斷的部分。

從分析結果裏能夠看出,尺寸佔比最大的是函數名。另外,大量咱們沒有使用到的函數,也在文件當中包含了。好比 std::net::ip::Ipv6Addr ,咱們根本沒用到。

爲何會這樣呢?最大的問題在於咱們引入了 std 這個 「包裝箱」(英文爲 crate,Rust 社區對包的稱呼)。

引入 std 的緣由主要在兩方面。

首先,在初始化 crc32 查找表時,代碼採用了 lazy_static 這個包裝箱提供的功能,它可以初始化一些比較複雜的變量——好比咱們的查找表。但其實查找表是固定的,我徹底能夠寫成純靜態的。這個 lazy_static 是從 rust-snappy 裏複製的,如今能夠我幹掉它,本身在源碼中直接寫出構造好的查找表。

其次,咱們代碼裏使用了 std::slice::from_raw_parts 這個來自 std 的方法,來把指針轉換爲數組。對於我這個 Rust 新手來講,這個就有些懵了。爲此,我專門在 StackOverflow 上求解了一番,換用 core::slice::from_raw_parts 來進行轉換。

這樣,咱們就能夠擺脫掉 std 了:

實際擺脫掉 std 須要多作一些其餘事情,你們能夠在源碼的src/lib-shrinked.rs文件中詳細查看。縮減 Rust 編譯結果的體積是一個比較繁瑣的話題,且根據 Rust 版本不一樣而不一樣,具體你們能夠參考官方的指南

webpack v4.12.0

8af21121a96f83596bfa
  size     name  module                                                           status
  489 B    0     (webpack)/buildin/global.js                                      built
  32.4 kB  1     ./fixture/jquery.js.txt                                          built
  13.2 kB  2     ./target/wasm32-unknown-unknown/release/wasm_crc32_example.wasm  built
  1.8 kB   8     ./crc32.js                                                       built
  497 B    10    (webpack)/buildin/module.js                                      built
  2.25 kB  12    ./browser.js                                                     built

  size     name  asset                                                            status
  160 kB   main  app.js                                                           emitted

  Δt 3317ms (7 modules hidden)
複製代碼

不錯,如今 wasm_crc32_example.wasm 只佔 13.2 KB 了。這 13.2 KB 還能不能縮呢?其實仍是能縮的,但再縮下去須要犧牲一些性能了。緣由是咱們靜態的查找表一共須要 9 * 256 項數據,每項數據佔 4 字節,所以查找表自己就佔去了 9 KB。你們能夠去 ./target/wasm32-unknown-unknown/release/ 目錄下看看,其實真正 wasm 當中的代碼實際只有約 1KB ,但因爲 webpack 在打包二進制數據時使用了 base64 編碼,所以整個文件的尺寸發生了膨脹。

若是還想把查找表也去掉的話,就必需要在運行時動態生成查找表,性能一定會有一些犧牲。 JavaScript 版本的 crc32 查找表就是動態生成的,若是我把它硬編碼出來,它其實也是這麼大。

在咱們以前的性能測試中,咱們沒有將查找表的生成時間計入,所以還算公平。

總結

WebAssembly 雖然在計算時性能優異,但其實在實際使用中困難重重,有一些門檻能夠跨過,而有一些則須要等待標準進一步演化和解決。下面總結了幾個 Rust + WebAssembly 的坑:

  • WebAssembly 只支持整數和浮點數,其餘高級類型須要本身序列化和反序列化,這個過程可能會很是耗時,甚至成爲性能瓶頸
  • WebAssembly 的內存獨立,除了內存複製以外,沒有其餘共享 JavaScript 一側內存的方案
  • WebAssembly 的內存塊是分頁的,一頁內存塊64KB,須要處理更多內容時,要麼對內容進行拆分,要麼擴容內存塊,這樣代碼可能會更加複雜
  • Rust 編譯出的 WebAssembly 機器碼一般由於 std 模塊的參與而變得體積龐大,替換掉 std 是可能的,但須要花不少心思
    • 若是不加任何處理,編譯出的 WebAssembly 模塊有 600KB 多
    • 經過各類策略,我可以將代碼縮減到13.2 KB,這裏面有 9KB 是 crc32 算法所須要的表
    • 排除查找表所佔體積,實際 WebAssembly 機器碼所佔體積會比 JavaScript 略小,但通過 base64 編碼後會發生膨脹,在個人例子裏和 JavaScript 相比優點不明顯
  • WebAssembly 機器碼在調試上目前還沒法和 JavaScript 代碼並肩,調試比較困難
  • WebAssembly 目前只在部分瀏覽器版本中支持,平常使用仍然須要編寫 JavaScript 版本的代碼進行降級
  • 儘管 WebAssembly 已經很是接近彙編機器碼,但一些 CPU 高級指令並不在 WebAssembly 當中包含,而這些指令每每對性能有巨大提高
    • 例如 SIMD 、CRC32 等(對,有些 CPU 直接實現了 crc32)

固然,若是這些對你來講都不是問題,那麼 WebAssembly 依然能夠一戰。可是就我目前的觀察來看, WebAssembly 離平常開發還有不少路要走,但願它越變越好。

最後附上已經上傳至 Github 的源碼連接,你們能夠在其中探索。若是有錯漏之處,也歡迎開 Issues 給我,多謝了。

後續補遺

在本文成文以後,我和 Rust 社區的大佬們溝通後發現若是在 Rust 中啓用 LTO (連接時優化,一種優化技術),則會在編譯時自動移除大量 std 的內容,從而使最終的 wasm 文件體積顯著減少。

根據測算,若是不手動移除 std 依賴,生成的 wasm 文件大約 30KB ;手動移除後,是否啓用 LTO 沒有明顯變化。

將來在 Rust 編譯 WebAssembly 文件時啓用 LLD (LLVM提供的連接器)以後, wasm 文件體積會自動變小,再也不須要你們操心。

相關文章
相關標籤/搜索