Rust + WASM 大文件 MD5 (附代码)

2,019 阅读3分钟

WebAssembly (Wasm) 是一种面向 web 的低级字节码格式,它被设计用于高效地在 web 浏览器中运行。相比于 JavaScript,WebAssembly 在很多情况下能够提供更好的性能,特别是 CPU 密集型计算。

结论

经实测发现,使用 WebAssembly 计算大文件的 MD5 哈希值相较于 JavaScript 实现,耗时缩减了一半,这种改进不仅可以显著提升用户体验,还能够加速网页加载速度,提高整体性能。这种技术优化可以为用户提供更快速、更流畅的体验,特别是在处理大型文件或数据时,效果更为明显。

1. 从 0 开始用 rust + wasm 实现 MD5 工具

1.1 安装 rust 工具链 cargo

具体参考:www.rust-lang.org/tools/insta…

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
1.2 安装 wasm-pack

具体参考:rustwasm.github.io/wasm-pack/i…

cargo install wasm-pack
1.3 安装 cargo-generate
cargo install cargo-generate
1.4 创建一个 rust+wasm 项目
// 创建项目
cargo new --lib md5-util

// 安装依赖
cargo add wasm-bindgen
cargo add hex
cargo add md-5
1.4.1 Cargo.toml
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2021"

[dependencies]
hex = "0.4.3"
md-5 = "0.10.6"
wasm-bindgen = "0.2.92"

[lib]
crate-type = ["cdylib", "rlib"]
1.4.2 lib.rs,这段代码就是给导出一个 js 可以饮用的胶水方法,calculate_md5,用于计算 md5,入参类型为 Unit8Array,返回值类型为 String
// src/lib.rs
use md5::{Md5, Digest};
use wasm_bindgen::prelude::wasm_bindgen;

#[wasm_bindgen]
pub fn calculate_md5(input: &[u8]) -> String {
    let mut hasher = Md5::new();
    hasher.update(input);
    let result = hasher.finalize();
    hex::encode(result)
}

可以看一下生成的源码,具体就不赘述了。

image.png

1.4.3 编译 wasm
// 根目录执行
wasm-pack build --target web

这时候可以看到 pkg 里面就是编译结果,下面由一段 js 代码来示例如何使用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Calculate File MD5 Hash</title>
</head>

<body>
  <input type="file" id="fileInput">
  <button id="calculateBtn">Calculate MD5</button>
  <div id="result"></div>
  <script type="module">
    import init, { calculate_md5 } from './pkg/md5_util.js';

    // SIZE 的单位为页,一页64KB
    const SIZE = 10000;

    // 初始化 wasm
    init().then(instance => {
      instance.memory.grow(SIZE);
    });

    const calculateMD5 = () => {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];

      if (file) {
        const reader = new FileReader();
        reader.readAsArrayBuffer(file);
        console.time('读取文件耗时');

        reader.onload = (e) => {
          console.timeEnd('读取文件耗时');

          console.time('获取 MD5 耗时');
          const hash = calculate_md5(new Uint8Array(e.target.result));
          console.timeEnd('获取 MD5 耗时');
          document.getElementById('result').innerText = `MD5 hash of ${file.name}: 【${hash}】`;
        };
      } else {
        document.getElementById('result').innerText = 'Please select a file.';
      }
    }

    const calculateBtn = document.querySelector('#calculateBtn');
    calculateBtn.addEventListener('click', calculateMD5);
  </script>
</body>

</html>
1.4.4 在 WebWorker 中使用 wasm

这种大计算量的方法阻塞主线程是不友好的,我们可以让 wasm 运行在 Worker 里,不阻塞主线程。

主线程代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Calculate File MD5 Hash</title>
</head>

<body>
  <input type="file" id="fileInput">
  <button id="calculateBtn">Calculate MD5</button>
  <div id="result"></div>
  <script type="module">
    const worker = new Worker('./test_worker.worker.js');

    worker.onmessage = (message) => {
      const { action, value } = message.data;
      switch (action) {
        case 'md5_result':
          console.timeEnd('获取 MD5 耗时');
          document.getElementById('result').innerText = `MD5: 【${value}】`;
          break;
      
        default:
          break;
      }
    };

    const calculateMD5 = () => {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];

      if (file) {
        const reader = new FileReader();
        reader.readAsArrayBuffer(file);
        console.time('读取文件耗时');

        reader.onload = (e) => {
          console.timeEnd('读取文件耗时');

          console.time('获取 MD5 耗时');
          const fileData = new Uint8Array(e.target.result);
          // 使用可转移对象,减少拷贝
          worker.postMessage({
            action: 'calculate_md5',
            value: fileData,
          }, [fileData.buffer]);

          // 不使用可转移对象
          // worker.postMessage({
          //   action: 'calculate_md5',
          //   value: fileData,
          // });
        };
      } else {
        document.getElementById('result').innerText = 'Please select a file.';
      }
    }

    const calculateBtn = document.querySelector('#calculateBtn');
    calculateBtn.addEventListener('click', calculateMD5);
  </script>
</body>

</html>

Worker:

let isInit = false;
let calculate_md5 = () => {};
// SIZE 的单位为页,一页64KB
const SIZE = 10000;

import('./pkg/md5_util.js').then(async (wasmJsModule) => {
  console.log('wasmJsModule: ', wasmJsModule);
  const init = wasmJsModule.default;
  calculate_md5 = wasmJsModule.calculate_md5;
  const instance = await init();
  instance.memory.grow(SIZE);
  isInit = true;
});

self.addEventListener('message', message => {
  const { action, value } = message.data;
  switch (action) {
    case 'calculate_md5': {
      const result = calculate_md5(value);
      console.log('result: ', result);
      self.postMessage({
        action: 'md5_result',
        value: result,
      });
      break;
    }
  
    default:
      break;
  }
});


2. 性能对比

文件大小js 耗时wasm + rust 耗时
119MB1123ms460ms

image.png