< 返回版块

2019-03-28 21:00    责任编辑:tk

标签:rust, webassembly

来源:https://mnt.io/2018/08/22/from-rust-to-beyond-the-webassembly-galaxy/

这篇博客文章是这一系列解释如何将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到一个字节序列,也就是让它变得扁平,否则返回空的字节序列。

扁平的AST

解析器的流程:左边的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*u8u32
  • 下面,如果这个节点是一个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
    )
}

好了,allocdeallocroot以及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项目的一部分,用来优化二进制,
  • gzipbrotil用来压缩二进制。

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

$ # 垃圾收集未使用数据.
$ 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大小: 1398208027wordpress-logo-simplified-rgb

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

WebAssembly 🚀 Javascript

WASM to JS

这部分,我们假设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个函数: allocdeallocroot。他们可以在导出属性里面被找到,还有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函数里才会被释放,因为在当前这一步它的长度还是未知的。

很好!我们现在有两个函数需要实现:writeBufferreadNodes

把数据写入内存

我们重第一个开始,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虚拟机优化。很重要的是这不是最原始的代码,原始的代码比这个优化得更多,但是还是很相似。

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

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

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

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

WebAssembly 🚀 NodeJS

WASM to NodeJS

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

  • 在NodeJS中没有Fetch API,因此WebAssembly二进制文件只能通过buffer直接实例化,像这样:WebAssembly.instantiate(fs.readFileSync(url), {}),
  • TextEncoderTextDecoder也没有在全局对象里面,他们在util.TextEncoderutil.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越多的往后旅行,也会变得更加有趣。

谢谢阅读!