< 返回版块

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

标签:rust, webassembly,asm.js

来源: https://mnt.io/2018/08/28/from-rust-to-beyond-the-asm-js-galaxy/

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

Rust解析器将要探索的第二个星系是ASM.js。这篇文章会解释什么是ASM.js,怎样编译博客解析器到ASM.js以及如何在浏览器中和Javascript一起使用ASM.js. 使用ASM.js的目标是当作WebAssembly不可用的备用方案。我强烈建议你读读前一篇关于WebAssembly的文章,因为他们有很多共同的地方

#什么是ASM.js,为什么需要ASM.js

Web应用的主要语言是Javascript,任何想要运行在Web上的应用都必须编译成Javascript,比如游戏。但是这里有个问题:编译输出文件非常重(即使是WebAssembly),这样Javascript虚拟机将很难对这样的代码做优化,这就导致运行缓慢或者执行低效(可以考虑用游戏的规模来做例子)。而且在Javascript为编译目标的情况下,一些语言基础设施变得毫无用处,比如eval

如果有一种新的语言可以作为编译目标,而且也能被Javascript虚拟机运行会怎么样?这就是WebAssembly,但是在2013年的时候解决方案就是ASM.js:

asm.js, 是一个严格的Javascript子集,它能够被用作编译器输出的底层的,高效的目标语言。这个子语言高效的描述>了一个沙盒虚拟机,可以适用于内存不安全的语言,像C或者C++。静态和动态的组合校验让Javascript可以对有效 的asm.js代码使用一种叫做ahead-of-time(AOT)的编译时优化策略。

因此ASM.js程序其实只是一个普通的Javascript程序。它不是一个新的语言而是Javascript的一个子集。它可以被任何Javascript虚拟机执行。但是,有个特殊的魔法声明use asm;,会指示虚拟机用ASM.js引擎来优化这个程序。

ASM.js通过算术运算引入了类型作为标记系统。比如,x|0表示x是一个整数,+x表示x是一个双精度小数,fround(x)表示x是一个浮点数。下面的例子声明了一个函数fn increment(x: u32) -> u32:

function increment(x) {
    x = x | 0;
    return (x + 1) | 0;
}

另一个重要的区别是ASM.js以模块的方式来运行,以和Javascript隔离。这个模块是一个需要3个参数的函数:

  • stdlib,一个带有引用到标准库API的对象
  • foreign,一个带有用户定义功能的对象(比如通过WebSocket发送一些东西)
  • heap,一个表示内存的数组(因为内存是手动管理的)

但是它仍然是Javascript。因此一个好消息是如果你的虚拟机没有对ASM.js做优化,那么它运行起来就是普通的Javascript程序,如果有优化,那么你就可以得到不错的性能提升。 macro4b

这个图片展示了3个基准测试,分别对于不同的Javascript引擎:Firefox, Firefox+asm.js, Google, 和Native。

记住ASM.js是被设计为一种编译目标。因此一般你不需要特别关心,因为那是编译器的活。对把C或者C++编译到Web的一个典型的编译和执行流程如下 1yoy1fa

Emscripten,如上图所示,是一个在这个Web平台演进过程中非常重要的一个项目。

它是一个用来编译输出asm.js和WebAssembly的工具链,基于LLVM之上,能够让C和C++程序以接近原生应用的速 度运行在Web上,而且不需要任何插件。

如果你和ASM.js或者WebAssembly打交道,迟早有一天你会看到这个名字。 I will not explain deeply what ASM.js is with a lot of examples. I recommend instead to read Asm.js: The Javascript Compile Target by John Resig, or Big Web app? Compile it! by Alon Zakai. 我不会用大量的例子来深入的解释ASM.js.我推荐两本书: Asm.js: The Javascript Compile Target by John Resig, 或者 Big Web app? Compile it! by Alon Zakai.

我们的过程有点不一样。我们不会直接编译Rust代码到ASM.js,而是先编译为WebAssembly,然后再编译为ASM.js。

#Rust 🚀 ASM.js

Rust to ASM.js

这个篇章会非常的短,应该说是最简单的一篇。要编译Rust到ASM.js你需要先编译到WebAssembly(参考前一篇文章)然后再编译WebAssembly二进制到ASM.js。

事实上真正需要ASM.js的地方是那些不支持WebAssembly的浏览器,比如IE。它只是我们在Web运行我们程序的一个后备方案。

下面看看这个流程:

  • 编译你的Rust项目到WebAssembly
  • 编译你的WebAssembly二进制为ASM.js模块
  • 优化和精简ASM.js模块

wasm2js会是你最好的朋友,它用来编译你的WebAssembly二进制为ASM.js模块,它属于Binaryen项目。下面假设我们已经有了程序的WebAssembly二进制,只需要运行下面的命令:

$ wasm2js --pedantic --output gutenberg_post_parser.asm.js gutenberg_post_parser.wasm

在这一步,gutenberg_post_parser.asm.js 有212kb。这个文件包含ECMAScript 6代码。注意这里因为考虑了老浏览器如IE,所以代码需要一点小小的转换来优化和精简ASM.js模块,我们用uglify-es工具,如下:

$ #  转换代码, 嵌入入到一个函数.
$ sed -i '' '1s/^/function GUTENBERG_POST_PARSER_ASM_MODULE() {/; s/export //' gutenberg_post_parser.asm.js
$ echo 'return { root, alloc, dealloc, memory }; }' >> gutenberg_post_parser.asm.js

$ # 精简代码.
$ uglifyjs --compress --mangle --output .temp.asm.js gutenberg_post_parser.asm.js
$ mv .temp.asm.js gutenberg_post_parser.asm.js

就像我们优化WebAssembly二进制一样,我们也可以gzipbrotli压缩输出文件:

$ # Compress.
$ gzip --best --stdout gutenberg_post_parser.asm.js > gutenberg_post_parser.asm.js.gz
$ brotli --best --stdout --lgwin=24 gutenberg_post_parser.asm.js > gutenberg_post_parser.asm.js.br

最终我们得到了下面的文件尺寸:

  • .asm.js: 54kb,
  • .asm.js.gz: 13kb,
  • .asm.js.br: 11kb.

都是非常小的!

思考一下,这里面涉及到了很多的转换:从Rust到WebAssembly到Javascript/ASM.js。。。工具的数量相对于工作量是非常少的。这展现了一个良好设计的流水线和不同工作组的人的良好合作。

题外话:如果你在读这篇博客,我假设你是一个开发者。既然如此,我很确定你可以花几个小时阅读源代码就像欣赏大师的画作。但是你有没有想过把Rust编译出来的Javascript输出是什么样的?

A Rust program compiled as WebAssembly compiled as ASM.js.

我可以说非常的喜欢它!

#ASM.js 🚀 Javascript

输出的gutenberg_post_parser.asm.js文件包含一个唯一的函数叫做:GUTENBERG_POST_PARSER_ASM_MODULE,它返回一个对象包含了4个私有函数。

  1. root,语法根
  2. alloc,来分配内存
  3. dealloc,释放内存
  4. memory,内存缓冲区

如果你看过上一篇WebAssembly的文章那么你会对这些概念感到熟悉。不要指望root会返回一个完整的AST,它只会返回一个内存指针,数据需要进一步编解码,也需要用同样的方式对内存进行读写。是的,相同的方式。因此边界层代码完全是一样的。你是否还记得在WebAssembly中作为Javascript边界的Module对象?那和GUTENBERG_POST_PARSER_ASM_MODULE函数返回的完全是一样的。你甚至可以用这个对象替换。

所有的代码都在这里。它完全重用WebAssembly的Javascript边界层代码,只是Module有一些不一样,也没有加载WebAssembly二进制。结果是ASM.js边界代码只用了34行就写出来了,压缩后仅有218个字节。

#结论

我们已经看到ASM.js可以在只支持Javascript的环境中(像IE)作为WebAssembly的备用方案,并可适配环境打开或者关闭ASM.js优化。

输出的ASM.js文件以及其边界层代码都非常的小。设计上,ASM.js边界层代码重用WebAssembly的Javascript边界层代码,因此同样只有一小部分外部接口代码需要审查和维护,这非常的有用。

我们已经在前一篇文章中看到Rust很快的速度。我们也已经在比较WebAssembly版本和纯Javascript版本解析器中看到同样速度结论。然而这个结论是否也适用于ASM.js模块呢?其实在这种情况下ASM.js只是一个备用方案,和其他备用方案一样通常都比较明显的慢于目标实现。下面是一个将Rust解析器作为ASM.js模块运行的基准测试:

test Javascript parser (ms) Rust parser as an ASM.js module (ms) speedup
demo-post.html 15.368 2.718 × 6
shortcode-shortcomings.html 31.022 8.004 × 4
redesigning-chrome-desktop.html 106.416 19.223 × 6
web-at-maximum-fps.html 82.92 27.197 × 3
early-adopting-the-future.html 119.880 38.321 × 3
pygmalian-raw-html.html 349.075 23.656 × 15
moby-dick-parsed.html 2,543.75 361.423 × 7

ASM.js模块版本的Rust解析器比纯Javascript实现平均快6倍。中位数也是6倍。这和WebAssembly版本比较还是有较大差距,但是考虑到这是个备用方案,而且平均已经快了6倍了,已经很不错了!

因此不仅是整个工作流因为Rust而变得更加安全,而且得到的结果也比Javascript快。

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

谢谢阅读!