WebAssembly 在最近幾年裏能夠說是如火如荼了。從基於 LLVM 的 Emscripten ,到嘗試打造全流程工具鏈的 binaryen ,再到 Rust 社區出現的wasm-bindgen……如今 webpack 4 已經內置了wasm
的引入,甚至連 Go 社區也不甘落後地推出了相關的計劃。javascript
做爲一個普通的前端開發者,我雖然一直在關注它的發展,但始終沒有直接操刀使用的機會。直到最近,咱們想使用 crc32 算法作一些字符串校驗時,我想看看 WebAssembly 是否可以在這項計算任務上,比原生 JavaScript 更具備性能優點。html
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
如今咱們新建一個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 自己是很是相似機器碼的一種語言,它緊湊且使用二進制來表達,所以在體積上自然有優點。但要讓開發者去寫機器碼,開發成本會很是高,所以伴隨着 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 社區和 JavaScript 社區有一些類似,你們都是樂於在工程化上投入精力,並致力於提高開發溫馨度的羣體。搭建一個 Rust 開發環境其實很是簡單,總共只須要3步:
rustup
。這一步和安裝 nvm
差很少。rustup
來安裝和使用 nightly
版的 rust。這一步至關於使用 nvm
安裝具體的 Node.js 版本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 以後,會自帶一個名爲 cargo 的命令行。cargo 是 Rust 社區的包管理命令行工具,比較相似於 Node.js 社區的 npm 。建立 Rust 項目能夠直接使用 cargo 進行:
cargo new crc32-example
複製代碼
這樣咱們就能夠在當前目錄下建立一個新目錄 crc32-example
,並在其中初始化好了咱們的代碼。cargo 默認會新建兩個文件,分別是 Cargo.toml
和 lib.rs
(具體代碼可參見文末的源碼),他們的做用分別是:
Cargo.toml
至關因而 Rust 社區的 package.json
,用於存放依賴描述和一些項目元信息。lib.rs
是代碼的入口文件,之後咱們寫的 Rust 代碼就會放在其中。下面咱們會詳細說說 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-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
,以便讓編譯器充分地信任咱們所寫的代碼。
好了,如今 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 當中的狀況,瀏覽器中會不會有什麼不一樣呢?因而我嘗試了一下在瀏覽器中進行測試。
除了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 的坑:
std
模塊的參與而變得體積龐大,替換掉 std
是可能的,但須要花不少心思
固然,若是這些對你來講都不是問題,那麼 WebAssembly 依然能夠一戰。可是就我目前的觀察來看, WebAssembly 離平常開發還有不少路要走,但願它越變越好。
最後附上已經上傳至 Github 的源碼連接,你們能夠在其中探索。若是有錯漏之處,也歡迎開 Issues 給我,多謝了。
在本文成文以後,我和 Rust 社區的大佬們溝通後發現若是在 Rust 中啓用 LTO (連接時優化,一種優化技術),則會在編譯時自動移除大量 std
的內容,從而使最終的 wasm 文件體積顯著減少。
根據測算,若是不手動移除 std
依賴,生成的 wasm 文件大約 30KB ;手動移除後,是否啓用 LTO 沒有明顯變化。
將來在 Rust 編譯 WebAssembly 文件時啓用 LLD (LLVM提供的連接器)以後, wasm 文件體積會自動變小,再也不須要你們操心。