使用 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 文件体积会自动变小,再也不须要你们操心。

相关文章
相关标签/搜索