30分鐘零基礎實戰 WebAssembly 和 Rust

語言: CN / TW / HK

關注「 Rust程式設計指北 」,一起學習 Rust,給未來投資

前言

  • 目標:通過實戰,理解 WebAssembly 的開發流程,對效能有個簡單對比

  • 示例:實現 leetcode - 斐波那契數列 [1]

  • 學習準備:需要 0 基礎,基於 Mac 環境, 因此 bash 命令列的部分 Windows 的同學可能需要替換下

  • 學習時長:約 30 分鐘

  • 打包工具: webpack 5

  • 寫作時間:2021-10-13

  • 程式碼@Github: dive-into-wasm [2]

1. 環境安裝

安裝 Rust 和對應的包。

1.1 安裝 Rust

$ curl -sSf https://static.rust-lang.org/rustup.sh | sh
複製程式碼

1.2 更換為國內源

更換為國內源,否則安裝太慢了。

新建檔案: ~/.cargo/config ,內容替換為如下, replace-with 這行可自己 ping 檔案中各個國內源頭,看哪個源快用哪個:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"

# 替換成你偏好的映象源
replace-with = 'sjtu'

# 清華大學
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# 中國科學技術大學
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大學
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# rustcc社群
[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"
複製程式碼

1.3 安裝 cargo-generate 腳手架

$ cargo install cargo-generate
複製程式碼

1.4 安裝 wasm-pack

wasm-packRust 編譯為 WebAssembly

$ cargo install wasm-pack
複製程式碼

2. 完成一個 Rust lib 專案

2.1 建立 Rust 專案

$ cargo new rust --lib
複製程式碼

這裡 rust 是專案的名稱,你可以換成你想要的任意名稱。

注意:會自動生成名為 rust 的資料夾,不要手動建立這個資料夾。

2.2 配置 Cargo.toml

新增 [lib] , 在 [dependencies] 下增加 wasm-bindgen 依賴,修改後的 完整內容類似如下:

[package]
name = "rust"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen="0.2"
複製程式碼

2.3 lib.rs 程式碼實現

程式碼細節此處不講了,檔案內容如下:

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fib(i: u32) -> u32 {
    match i {
        0 => 0,
        1 => 1,
        _ => fib(i - 1) + fib(i - 2)
    }
}

#[wasm_bindgen]
pub fn fib_tail_call_optimized(i: u32, prev: u32, next: u32) -> u32 {
    match i {
        0 => next,
        1 => next,
        _ => fib_tail_call_optimized(i - 1, next, prev + next)
    }
}
複製程式碼

2.4 編譯為 WebAssembly 二進位制

在 rust 專案根目錄,用 wasm-pack 工具編譯:

$ wasm-pack build
複製程式碼

成功的話會輸出類似如下內容:

...
   Your wasm pkg is ready to publish at .../dive-into-wasm/rust/pkg.
複製程式碼

此時在 pkg 目錄會生成如下 5 個檔案: rust.jsrust.d.tsrust_bg.jsrust_bg.wasmrust_bg.wasm.d.tsrust_bg.wasm 就是二進位制檔案,我們待會要用到的就是這個了。

3. 完成一個前端專案

完成一個前端專案,並編譯到瀏覽器執行生成的 WebAssembly 。

3.1 建立前端專案

rust 目錄的父目錄執行(即 web 目錄和 rust 目錄是平級的,否則本教程的示例程式碼中的路徑要調整了):

$ mkdir web && cd web
$ npm init -y
複製程式碼

3.2 配置 webpack 和開發伺服器

安裝依賴:

$ npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
複製程式碼

Webpack 5 預設不開啟 WebAssembly 支援,需要手動配置一下,同時用 html-webpack-plugin 自動生成一個入口 HTML , webpack.config.js 內容如下:

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'development',
    plugins: [
        new HtmlWebpackPlugin({title: '實戰 WASM Rust'})
    ],
    // 實驗特性
    // BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental feature.
    // You need to enable one of the WebAssembly experiments via 'experiments.asyncWebAssembly: true' (based on async modules) or 'experiments.syncWebAssembly: true' (like webpack 4, deprecated).
    experiments: {
        asyncWebAssembly: true
    }
}
複製程式碼

3.3 編寫代用和測試程式碼

實現呼叫 WebAssembly 並增加 JS 實現 ,進行 benchmark 。

JS 實現 2 個方法,一個是普通實現,一個是尾遞迴優化的實現, src/fib.js 程式碼:

function fib(i) {
    if (i <= 1) return i

    return fib(i - 1) + fib(i - 2)
}


function fibTailCallOptimized(i, prev = 0, next = 1) {
    if (i <= 1) return next

    return fibTailCallOptimized(i - 1, next, prev + next)
}

export {
    fib,
    fibTailCallOptimized
}
複製程式碼

呼叫和執行 benchmark , src/index.js 程式碼:

// 直接引用 wasm 檔案
import {fib as wasm_fib, fib_tail_call_optimized as wasm_fib_tail_call_optimized } from '../../rust/pkg/rust_bg.wasm'
import { fib, fibTailCallOptimized } from './fib.js'

function time(timerName, func) {
    console.time(timerName)
    console.log(`${timerName}: `, func())
    console.timeEnd(timerName)
}

// js 實現數字不能太大,否則 CPU 佔滿,執行不出來結果
const num = 30
time('wasm_fib', () => wasm_fib(num))
time('wasm_fib_tail_call_optimized', () => wasm_fib_tail_call_optimized(num, 0, 1))
time('fib', () => fib(num))
time('fibTailCallOptimized', () => fibTailCallOptimized(num))
複製程式碼

注意第一行引用了 rust_bg.wasm 二進位制程式碼。

注意:瀏覽器安全策略禁止通過 file:// 協議載入 wasm 檔案,所以我們這裡使用的 webpack-dev-server

3.4 瀏覽器執行及 benchmark

webpack 打包並在瀏覽器執行測試效果。

$ npx webpack serve
複製程式碼

在瀏覽器開啟,通常是 http://localhost:8080/ 。控制檯輸入類似如下(已刪除結果的展示,你可以自己檢查下,首先保證執行結果的一致性):

wasm_fib: 0.193115234375 ms
wasm_fib_tail_call_optimized:   ms

fib: 0.93701171875 ms
fibTailCallOptimized: 0.16796875 ms
複製程式碼

可以看到 wasm 執行效率比 JS 未做優化的高 5 倍左右;

尾呼叫優化後是 2 倍左右。

作者:Bug 王

連結:https://juejin.cn/post/7018378112228392974

參考資料

[1]

leetcode - 斐波那契數列: https://leetcode-cn.com/problems/fibonacci-number/

[2]

dive-into-wasm: https://github.com/jsbugwang/dive-into-wasm

推薦閱讀

覺得不錯,點個贊吧

掃碼關注「 Rust程式設計指北