这篇博客文章是这一系列解释如何将Rust发射到地球以外的许多星系的文章的一部分:

  • 前奏
  • WebAssembly 星系(当前这一集),
  • ASM.js星系
  • c 星系
  • PHP星系,以及
  • NodeJS 星系

我们的Rust解析器将探索的第一个星系是WebAssembly (WASM)星系。本文将解释什么是WebAssembly,如何将我们的解析器编译成WebAssembly,以及如何在浏览器中的Javascript或者NodeJS一起使用WebAssembly二进制文件。

什么是WebAssembly,为什么需要WebAssembly?

如果您已经了解WebAssembly,可以跳过这一部分。

WebAssembly的定义如下:

WebAssembly(缩写:Wasm)是一种基于堆栈虚拟机的二进制指令格式。Wasm被设计为是可移植的目标格式,可将高级语言(如C/ C++ /Rust)编译为Wasm,使客户端和服务器端应用程序能部署在web上。

我还需要说更多吗?也许是的…

WebAssembly是一种新的可移植二进制格式。像C、C++或Rust这样的语言已经能够编译到这个目标格式。它是ASM.js的精神的继承者。我所说的精神继承者,是指都是相同的一群试图扩展Web平台和使Web变得更快的人,他们同时使用这两种技术,他们也有一些共同的设计理念,但现在这并不重要。

在WebAssembly之前,程序必须编译成Javascript才能在Web平台上运行。这样的输出文件大部分时间都很大。因为Web是基于网络的文件必须下载,这是很耗时的。WebAssembly被设计成一种大小和加载时高效的二进制格式。

从很多方面来看,WebAssembly也比Javascript更快。尽管工程师们在Javascript虚拟机中进行了各种疯狂的优化,但Javascript是一种弱动态类型语言,需要解释运行。WebAssembly旨在利用通用的硬件功能以原始速度执行。WebAssembly的加载速度也比Javascript快,因为解析和编译是在二进制文件从网络传输时进行的。因此,一旦完成了二进制文件下载,它就可以运行了:无需在运行程序之前等待解析器和编译器。

当前我们就已经能够编写一个Rust程序,并将其编译在Web平台上运行,我们的博客系列就是一个完美的例子,为什么要这么做呢? 因为WebAssembly已经在所有主流浏览器实现,而且因为它是为Web而设计的:在Web平台上(像浏览器一样)生存和运行。但是,它的可移植性、安全性和沙箱内存设计使其成为在Web平台之外运行的理想选择(请参阅无服务器的WASM框架或为WASM构建的应用程序容器)。

我认为需要强调的时候,WebAssembly并不是用来替代Javascript的。它只是另一种技术,它解决了我们今天可能遇到的许多问题,比如加载时间、安全性或速度。

##Rust🚀WASM

Rust WASM团队致力于推动通过一组工具集来将Rust编译到WebAssembly。有一本书解释如何用Rust编写WebAssembly程序。

对于Gutenberg Rust解析器,我没有使用像wasm-bindgen这样的工具(这是一个纯粹的gem),因为在几个月前开始这个项目的时候我遇到了一些限制。请注意,其中一些已经被解决了!无论如何,我们将手工完成大部分工作,我认为这是理解这背后工作原理的一个很好的方法。当您熟悉了和WebAssembly交互时,wasm-bindgen是一个非常好的工具,您可以很容易地获得它,因为它抽象了所有交互,让您更能关注代码逻辑。

我想要提醒读者的是Gutenberg的Rust解析器开放了一个AST以及一个root函数(语法的根),相应的定义如下

pub enum Node<'a> {
Block {
name: (Input<'a>, Input<'a>),
attributes: Option<Input<'a>>,
children: Vec<Node<'a>>
},
Phrase(Input<'a>)
}

pub fn root(
input: Input
) -> Result<(Input, Vec<ast::Node>), nom::Err<Input>>;

知道了这个我们就可以开始了!

通用设计

下面是我们的通用设计或者说流程:

  1. Javascript将博客内容解析为WebAssembly模块的内存
  2. 传入这个内存指针以及博客长度来调用root函数
  3. Rust从内存中读到博客内容,运行Gutenberg解析器,编译AST的结果到一个字节序列,然后将这个字节序列的指针返回给Javascript
  4. Javascript从这个指针读取内存,解码这一个序列为Javascript对象得到具有友好API的AST

为什么是字节序列?因为WebAssembly只支持整数和浮点数,不支持字符串也不支持数组,也因为Rust解析器恰好也需要字节切片,正好方便使用。

我们使用边界层来表示这部分负责读写WebAssembly内存的代码,它也负责暴露友好的API。

现在我们把焦点放到Rust代码上,它包含四个函数:

  • ​alloc​​用来分配内存(导出函数),
  • ​dealloc​​用来释放内存(导出函数),
  • ​root​​运行解析器(导出函数),
  • ​into_bytes​​用来转换AST到字节序列

所有的代码都在这里了,大约150行。我们来解读一下。

内存分配

我们从内存分配器开始。我选择了​​wee_alloc​​来作为内存分配器。它是专为WebAssembly设计的,小巧(1K以内)而高效。

下面的代码描述了内存分配器的构建以及我们代码“前奏”(开启一些编译器功能,比如alloc,声明外部crates,一些别名,还声明了必要的函数比panic,oom等等)。可以认为他们是样板:

#![no_std]
#![feature(
alloc,
alloc_error_handler,
core_intrinsics,
lang_items
)]

extern crate gutenberg_post_parser;
extern crate wee_alloc;
#[macro_use] extern crate alloc;

use gutenberg_post_parser::ast::Node;
use alloc::vec::Vec;
use core::{mem, slice};

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::intrinsics::abort(); }
}

#[alloc_error_handler]
fn oom(_: core::alloc::Layout) -> ! {
unsafe { core::intrinsics::abort(); }
}

// 这是 `std::ffi::c_void`的定义, 但是在我们这个下面里面 WASM 的运行不需要 std.
#[repr(u8)]
#[allow(non_camel_case_types)]
pub enum c_void {
#[doc(hidden)]
__variant1,

#[doc(hidden)]
__variant2
}

Rust内存就是WebAssembly内存。Rust将会自己负责分配和释放内存,但是Javascipt需要来分配和释放WebAssembly的内存来通信或者说交换数据。因此我们需要导出内存分配和释放的函数。

再一次,这个基本就是样板。alloc函数创建一个空的指定长度的数组(因为它是一个顺序内存段)并且返回这个空数组的指针。

#[no_mangle]
pub extern "C" fn alloc(capacity: usize) -> *mut c_void {
let mut buffer = Vec::with_capacity(capacity);
let pointer = buffer.as_mut_ptr();
mem::forget(buffer);

pointer as *mut c_void
}

注意​​#[no_mangle]​​​特性指示Rust编译器不去混淆函数名字,也就是不去重命名符号。用​​extern "C"​​用来导出WebAssembly里面的函数,因此从WebAssembly二进制外面看起来他们就是公开的。

这个代码其实很直观,和我们先前说明的一样: ​​Vec​​​是分配的一个指定长度的数组,返回值是指向这个数组的指针。重要的部分是​​mem::forget(buffer)​​​,这个是必须的,这样Rust在这个数组离开作用域的时候不会去释放它。事实上Rust是强制RAII的,意味着一个对象一段离开作用域,它的析构函数会被调用并且它拥有的资源也会被释放。这种行为是用来防御资源泄露bug的,这也是为什么我们可以不用手动释放内存也不用担心Rust内存泄露(看看RAII的例子)。在这个情况下,我们希望分配内存并且保持甚至到函数结束执行,因此需要调用​​mem::forget​​.

我们来看看​​dealloc​​函数。目标是根据一个指针和其容量长度来重建数组,并且让Rust释放它:

#[no_mangle]
pub extern "C" fn dealloc(pointer: *mut c_void, capacity: usize) {
unsafe {
let _ = Vec::from_raw_parts(pointer, 0, capacity);
}
}

这里​​Vec::from_raw_parts​​​函数被标记为​​unsafe​​​,因为我们要用​​unsafe​​块来隔离它,让它被Rust认为是安全的。

变量​​_​​包含我们要释放的数据,并且它立即就离开了作用域,所有Rust会自动的释放它。

从输入到扁平的AST

现在开始绑定的核心部分!​​root​​函数基于指针和长度读取博客内容来,然后解析。如果结果正确它将序列化AST到一个字节序列,也就是让它变得扁平,否则返回空的字节序列。

从Rust到远方:WebAssembly 星系_解析器

解析器的流程:左边的input将会被解析为AST,然后这个AST会被序列化为右边扁平的字节序列。

#[no_mangle]
pub extern "C" fn root(pointer: *mut u8, length: usize) -> *mut u8 {
let input = unsafe { slice::from_raw_parts(pointer, length) };
let mut output = vec![];

if let Ok((_remaining, nodes)) = gutenberg_post_parser::root(input) {
// 编译 AST (nodes) 到字节序列.
}

let pointer = output.as_mut_ptr();
mem::forget(output);

pointer
}

​input​​​变量包含了博客文章。它是根据一个指针和其长度得到的内存。output变量是会被作为返回值的字节序列。​​gutenberg_post_parser::root(input)​​开始运行解析器。如果解析成那么节点会被编译为字节序列(现在先忽略不讲)。然后我们可以得到指向这个字节序列的指针,Rust编译器被指定为不去释放它,最后这个指针被返回。再一次想说这个逻辑其实很直观。

现在我们聚焦在AST到字节序列(​​u8​​)的编译上。因为AST里面的数据已经是字节了,所有这个处理过程变得相对简单。我们的目标是扁平化这个AST

  • 开头四个字节表示第一层的节点数量(4*​​u8​​即​​u32​​)
  • 下面,如果这个节点是一个Block(模块):
  • 第一个字节是节点类型:​​1u8​​ 表示​​block​
  • 第二个字节是模块名字的长度
  • 第三到第六个字节是所有属性的长度
  • 第七个字节是字节点数量
  • 下一个字节是模块名字
  • 再下一个是具体的一些属性(如果没有表示为:​​&b"null"[..]​​),
  • 在下面是字节点的字节序列
  • 如果节点是一个短语:
  • 第一个字节是节点类型:​​2u8​​ 表示​​phrase​​(短语)
  • 第二到第十五个字节表示短语的长度。
  • 后面的字节是​​phrase​​本身。

补充一些​​root​​函数的代码:

if let Ok((_remaining, nodes)) = gutenberg_post_parser::root(input) {
let nodes_length = u32_to_u8s(nodes.len() as u32);

output.push(nodes_length.0);
output.push(nodes_length.1);
output.push(nodes_length.2);
output.push(nodes_length.3);

for node in nodes {
into_bytes(&node, &mut output);
}
}

下面是into_bytes函数:

fn into_bytes<'a>(node: &Node<'a>, output: &mut Vec<u8>) {
match *node {
Node::Block { name, attributes, ref children } => {
let node_type = 1u8;
let name_length = name.0.len() + name.1.len() + 1;
let attributes_length = match attributes {
Some(attributes) => attributes.len(),
None => 4
};
let attributes_length_as_u8s = u32_to_u8s(attributes_length as u32);

let number_of_children = children.len();
output.push(node_type);
output.push(name_length as u8);
output.push(attributes_length_as_u8s.0);
output.push(attributes_length_as_u8s.1);
output.push(attributes_length_as_u8s.2);
output.push(attributes_length_as_u8s.3);
output.push(number_of_children as u8);

output.extend(name.0);
output.push(b'/');
output.extend(name.1);

if let Some(attributes) = attributes {
output.extend(attributes);
} else {
output.extend(&b"null"[..]);
}

for child in children {
into_bytes(&child, output);
}
},

Node::Phrase(phrase) => {
let node_type = 2u8;
let phrase_length = phrase.len();

output.push(node_type);

let phrase_length_as_u8s = u32_to_u8s(phrase_length as u32);

output.push(phrase_length_as_u8s.0);
output.push(phrase_length_as_u8s.1);
output.push(phrase_length_as_u8s.2);
output.push(phrase_length_as_u8s.3);
output.extend(phrase);
}
}
}

在我看来比较有趣的是这个代码读起来就像上面无序列表很接近。

最让人好奇的当属下面这个函数u32_to_u8s:

fn u32_to_u8s(x: u32) -> (u8, u8, u8, u8) {
(
((x >> 24) & 0xff) as u8,
((x >> 16) & 0xff) as u8,
((x >> 8) & 0xff) as u8,
( x & 0xff) as u8
)
}

好了,​​alloc​​​, ​​dealloc​​​,​​root​​​以及​​into_bytes​​四个函数全部完成。

生成和优化WebAssembly二进制

要得到WebAssembly二进制,这个工程需要编译到​​wasm32-unknown-unknown​​​这个目标。目前我们需要​​nightly​​​工具链来编译我们的项目,当然这在后面可能会变化,因此你要确保用​​rustup update nightly​​​命令安装了最新的​​nightly​​版本的rustc和co。我们来运行cargo

$ RUSTFLAGS='-g' cargo +nightly build --target wasm32-unknown-unknown --release

这个WebAssembly二进制有22kb。我们的目标是减小这个尺寸,因此我们需要下面的工具:

  • ​wasm-gc​​​来做垃圾收集,包括没有使用到的​​imports​​,内部函数,类型等等。
  • ​wasm-snip​​用来标记不可达函数,这个工具对那些链接器没办法删除的未使用代码很有效。
  • ​wasm-opt​​,是Binaryen项目的一部分,用来优化二进制,
  • ​gzip​​​和​​brotil​​用来压缩二进制。

简单来说,我们就是要做下面的事情

$ # 垃圾收集未使用数据.
$ wasm-gc gutenberg_post_parser.wasm
$ # 标记不可达并移除.
$ wasm-snip --snip-rust-fmt-code --snip-rust-panicking-code gutenberg_post_parser.wasm -o gutenberg_post_parser_snipped.wasm
$ mv gutenberg_post_parser_snipped.wasm gutenberg_post_parser.wasm
$ # 再次垃圾收集未使用数据.
$ wasm-gc gutenberg_post_parser.wasm
$ # 优化二进制大小.
$ wasm-opt -Oz -o gutenberg_post_parser_opt.wasm gutenberg_post_parser.wasm
$ mv gutenberg_post_parser_opt.wasm gutenberg_post_parser.wasm
$ # 压缩.
$ gzip --best --stdout gutenberg_post_parser.wasm > gutenberg_post_parser.wasm.gz
$ brotli --best --stdout --lgwin=24 gutenberg_post_parser.wasm > gutenberg_post_parser.wasm.br

我们最终得到下面的不同大小的文件:

  • .wasm: 16kb,
  • .wasm.gz: 7.3kb,
  • .wasm.br: 6.2kb.

简洁!Brotil已经被大多数浏览器实现,因此如果客户端声称接受​​Accept-Encoding: br​​​,服务器就可以返回​​wasm.br​​文件

让你感受一些6.2kb可以表达什么,下面的图片就是6.2kb大小:

从Rust到远方:WebAssembly 星系_字节序_02

WebAssembly二进制马上就可以运行了!

WebAssembly 🚀 Javascript

从Rust到远方:WebAssembly 星系_javascript_03

这部分,我们假设Javascript是运行在浏览器里,因此我们需要做下面的流程:

  • 加载和实例化WebAssembly二进制,
  • 写入博客内容到WebAssembly模块内存,
  • 调用解析器的root函数,
  • 读取WebAssembly模块的内存来加载扁平的AST(字节序列)并解码来得到​​Javascript AST​​(用我们自己的对象)。

所有的代码都在这里,大约150行。我不会去解释所有的代码,因为有些代码的目的是为暴露给用户更友好的API。我将更专注于解释主要部分。

加载和实例化

WebAssembly API暴露了很多的方法来加载WebAssembly二进制。最理想的一种应该是使用​​WebAssembly.instanciateStreaming​​​函数,它会一边下载二进制同时进行编译,没有任何阻塞。这个API依赖​​Fetch API​​​。你可能会猜到的是:它是异步的(返回一个​​promise​​)。WebAssembly本身不是异步的,除非你用线程,但是实例化这一步却是异步的。当然也可以不这么做,只是会很奇怪,而且Chrome有一个4kb二进制大小的强限制,这将会使你很快就会放弃其它的尝试。

为了能够流式加载WebAssembly二进制,服务器也必须要发送​​Content-Type​​​头为​​application/wasm MIME​​类型。

让我们来实例化我们的WebAssembly

const url = '/gutenberg_post_parser.wasm';
const wasm =
WebAssembly.
instantiateStreaming(fetch(url), {}).
then(object => object.instance).
then(instance => { /* step 2 */ });

WebAssembly已经被实例化好了,我们可以开始下一步了。在运行解析器之前,最后在做点优化打磨

记住我们要在WebAssembly二进制暴露的3个函数: ​​alloc​​​, ​​dealloc​​​ 和 ​​root​​​。他们可以在导出属性里面被找到,还有​​memory​​也在这里面. 写出来就是这样:

then(instance => {
const Module = {
alloc: instance.exports.alloc,
dealloc: instance.exports.dealloc,
root: instance.exports.root,
memory: instance.exports.memory
};

runParser(Module, '<!-- wp:foo /-->xyz');
});

很好,所有准备工作都已经完成,可以开始些​​runParser​​函数了!

解析器的执行器

提醒一下,这个函数需要做下面的事情:把输入(博客内容)写入到WebAssembly模块的内存(​​Module.memory​​​),调用​​root​​​函数(​​Module.root​​),并且从WebAssembly模块的内存读取返回结果。

function runParser(Module, raw_input) {
const input = new TextEncoder().encode(raw_input);
const input_pointer = writeBuffer(Module, input);
const output_pointer = Module.root(input_pointer, input.length);
const result = readNodes(Module, output_pointer);

Module.dealloc(input_pointer, input.length);

return result;
}

具体来讲:

  • ​raw_input​​​ 通过​​TextEncoderAPI​​被编码成了字节序列,放到了​​input​​中。
  • 然后​​input​​通过​​writeBuffer​​写到了WebAssembly内存,返回对应的指针,
  • 然后root函数被调用,传入​​input​​和长度,返回的指针存到​​output​
  • 然后解码​​output​
  • 最后,​​input​​被释放。解析器的输出​​output​​只有在​​readNodes​​函数里才会被释放,因为在当前这一步它的长度还是未知的。

很好!我们现在有两个函数需要实现:​​writeBuffer​​​和​​readNodes​​。

把数据写入内存

我们重第一个开始,​​writeBuffer​​:

function writeBuffer(Module, buffer) {
const buffer_length = buffer.length;
const pointer = Module.alloc(buffer_length);
const memory = new Uint8Array(Module.memory.buffer);

for (let i = 0; i < buffer_length; ++i) {
memory[pointer + i] = buffer[i];
}

return pointer;
}

解读:

  • ​buffer_length​​​存入​​buffer​​的长度。
  • 内存中开辟一块空间来存​​buffer​​,
  • 然后我们实例化一个​​unit8​​类型的​​buffer​​视图,也就是说我们把这个​​buffer​​看作是一个​​u8​​的序列,这个就是Rust想要的,
  • 最后这个​​buffer​​被循环的复制到内存中,非常普通,然后返回指针。

需要注意的是,不像在C语言里面的的字符串我们需要在结尾加NULL, 这里只需要原始数据(在Rust里面我们只需要用slice::from_raw_parts读就可以了,因为slice是很简单的结构)

读取解析器的输出​​output​

在这一步,输入​​input​​​已经写进了内存,​​root​​​函数也得到了调用,也就是说解析器已经运行了。它返回了一个指向输出结果​​output​​的指针,我们现在要做的就是读取并解码它。

记住,前面4个字节编码的是我们要读取的节点数量。开始吧!

function readNodes(Module, start_pointer) {
const buffer = new Uint8Array(Module.memory.buffer.slice(start_pointer));
const number_of_nodes = u8s_to_u32(buffer[0], buffer[1], buffer[2], buffer[3]);

if (0 >= number_of_nodes) {
return null;
}

const nodes = [];
let offset = 4;
let end_offset;

for (let i = 0; i < number_of_nodes; ++i) {
const last_offset = readNode(buffer, offset, nodes);

offset = end_offset = last_offset;
}

Module.dealloc(start_pointer, start_pointer + end_offset);

return nodes;
}

解析:

  • 实例化一个内存的​​uint8​​视图,更准确的是:一个从​​start_pointer​​开始的内存切片
  • 先读取节点数量,然后读取所有节点,
  • 最后,解析器的输出​​output​​被释放。

这里记录一些​​u8s_to_u32​​​函数,完全就是和​​u32_to_u8s​​相反的功能:

function u8s_to_u32(o, p, q, r) {
return (o << 24) | (p << 16) | (q << 8) | r;
}

下面我贴出​​readNode​​函数,但是我不会做过多解释。这仅是对解析器输出的解码部分。

function readNode(buffer, offset, nodes) {
const node_type = buffer[offset];

// Block.
if (1 === node_type) {
const name_length = buffer[offset + 1];
const attributes_length = u8s_to_u32(buffer[offset + 2], buffer[offset + 3], buffer[offset + 4], buffer[offset + 5]);
const number_of_children = buffer[offset + 6];

let payload_offset = offset + 7;
let next_payload_offset = payload_offset + name_length;

const name = new TextDecoder().decode(buffer.slice(payload_offset, next_payload_offset));

payload_offset = next_payload_offset;
next_payload_offset += attributes_length;

const attributes = JSON.parse(new TextDecoder().decode(buffer.slice(payload_offset, next_payload_offset)));

payload_offset = next_payload_offset;
let end_offset = payload_offset;

const children = [];

for (let i = 0; i < number_of_children; ++i) {
const last_offset = readNode(buffer, payload_offset, children);

payload_offset = end_offset = last_offset;
}

nodes.push(new Block(name, attributes, children));

return end_offset;
}
// Phrase.
else if (2 === node_type) {
const phrase_length = u8s_to_u32(buffer[offset + 1], buffer[offset + 2], buffer[offset + 3], buffer[offset + 4]);
const phrase_offset = offset + 5;
const phrase = new TextDecoder().decode(buffer.slice(phrase_offset, phrase_offset + phrase_length));

nodes.push(new Phrase(phrase));

return phrase_offset + phrase_length;
} else {
console.error('unknown node type', node_type);
}
}

注意这个代码非常的简单,很容易的被Javascript虚拟机优化。很重要的是这不是最原始的代码,原始的代码比这个优化得更多,但是还是很相似。

好了!我们已经成功的从解析器读取结果并解码!我们只需要实现​​Block​​​和​​Phrase​​类:

class Block {
constructor(name, attributes, children) {
this.name = name;
this.attributes = attributes;
this.children = children;
}
}

class Phrase {
constructor(phrase) {
this.phrase = phrase;
}
}

最终的输出将是一个这种类型的对象数组。简单吧!

WebAssembly 🚀 NodeJS

从Rust到远方:WebAssembly 星系_解析器_04

Javascript和NodeJS版本有下面的一些差异:

  • 在NodeJS中没有​​Fetch API​​,因此WebAssembly二进制文件只能通过​​buffer​​直接实例化,像这样:​​WebAssembly.instantiate(fs.readFileSync(url), {})​​,
  • ​TextEncoder​​​和​​TextDecoder​​也没有在全局对象里面,他们在​​util.TextEncoder​​ 和 ​​util.TextDecoder​​里面.

为了能在这两个环境共享代码, 可以在一个.mjs文件中实现一个边界层(我们写的Javascript代码),也就是ECMAScript模块。我们就能够像下面这样写:​​import { Gutenberg_Post_Parser } from './gutenberg_post_parser.mjs'​​​,如果我们之前所有的代码是一个类。在浏览器端,脚本的加载方式是:​​<script type="module" src="…" />​​​,在NodeJS端,node需要带参数​​--experimental-modules​​运行。为了有个更全面的认识,我可以推荐你这个2018年JSConf的演讲:Please wait… loading: a tale of two loaders by Myles Borins

所有的代码在这里。

#结论

我们已经看到了如何容Rust写一个真正的解析器的细节,如何编译成WebAssembly二进制, 以及如何在Javaacript和NodeJS里面使用

这个解析器可以和普通的Javascript代码一起在浏览器端使用,也可以和NodeJS中以CLI的方式运行,也可以在任何支持NodeJS的平台。

加上产生WebAssembly的Rust代码和原生Javascript代码一共只有313行。相比于完全用Javascript来写,这个小小的代码集合更容易审查和维护。

另一个有点争议的点是安全和性能。Rust是内存安全的,我们都知道。它也有很高的性能,但是WebAssembly却不一定有这些特性,对吧?下面的表格展示了Gutenberg项目纯Javascript解析器(基于PEG.js实现)和本文的项目:Rust编译成WebAssembly二进制方案的一个基准测试对比结果:

文件

Javascript 解析器(毫秒)

Rust 实现的WebAssembly 解析器 (毫秒)

加速

demo-post.html

13.167

0.252

× 52

shortcode-shortcomings.html

26.784

0.271

× 98

redesigning-chrome-desktop.html

75.500

0.918

× 82

web-at-maximum-fps.html

88.118

0.901

× 98

early-adopting-the-future.html

201.011

3.329

× 60

pygmalian-raw-html.html

311.416

2.692

× 116

moby-dick-parsed.html

2,466.533

25.14

× 98

WebAssembly二进制比纯Javascript实现平均快86倍。中位数是98倍。有些边缘的用例很有趣,像moby-dick-parsed.html,纯Javascript版本用了2.5s而WebAssembly只用了25ms

因此,它不仅安全,而且在这个场景下比Javascript快。只有300行代码。

需要注意的是WebAssembly还不支持SIMD:还是这个提案。Rust也在慢慢的支持它(PR #549),他将能显著的提升性能!

在这个系列的后续文章中我们将会看到Rust会到达很多的星系,Rust越多的往后旅行,也会变得更加有趣。

谢谢阅读!