前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Rust到远方:WebAssembly 星系

从Rust到远方:WebAssembly 星系

作者头像
MikeLoveRust
发布2019-07-30 17:05:50
1.4K0
发布2019-07-30 17:05:50
举报

来源: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函数(语法的根),相应的定义如下

代码语言:javascript
复制
pub enum Node<'a> {
    Block {
        name: (Input<'a>, Input<'a>),
        attributes: Option<Input<'a>>,
        children: Vec<Node<'a>>
    },
    Phrase(Input<'a>)
}

代码语言:javascript
复制
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等等)。可以认为他们是样板:

代码语言:javascript
复制
#![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函数创建一个空的指定长度的数组(因为它是一个顺序内存段)并且返回这个空数组的指针。

代码语言:javascript
复制
#[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释放它:

代码语言:javascript
复制
#[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到一个字节序列,也就是让它变得扁平,否则返回空的字节序列。

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

代码语言:javascript
复制
#[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函数的代码:

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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

代码语言:javascript
复制
$ RUSTFLAGS='-g' cargo +nightly build --target wasm32-unknown-unknown --release

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

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

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

代码语言:javascript
复制
$ # 垃圾收集未使用数据.
$ 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大小:

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

WebAssembly ? Javascript

这部分,我们假设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

代码语言:javascript
复制
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也在这里面. 写出来就是这样:

代码语言:javascript
复制
        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模块的内存读取返回结果。

代码语言:javascript
复制
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

代码语言:javascript
复制
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个字节编码的是我们要读取的节点数量。开始吧!

代码语言:javascript
复制
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相反的功能:

代码语言:javascript
复制
function u8s_to_u32(o, p, q, r) {
    return (o << 24) | (p << 16) | (q << 8) | r;
}

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

代码语言:javascript
复制
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类:

代码语言:javascript
复制
class Block {
    constructor(name, attributes, children) {
        this.name = name;
        this.attributes = attributes;
        this.children = children;
    }
}

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

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

WebAssembly ? 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越多的往后旅行,也会变得更加有趣。

谢谢阅读!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust语言学习交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是WebAssembly,为什么需要WebAssembly?
  • 通用设计
  • 内存分配
  • 从输入到扁平的AST
  • 生成和优化WebAssembly二进制
  • WebAssembly ? Javascript
  • 加载和实例化
  • 解析器的执行器
  • 把数据写入内存
  • 读取解析器的输出output
  • WebAssembly ? NodeJS
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档