这篇博客文章是这一系列解释如何将Rust发射到地球以外的许多星系的文章的一部分:
- 前奏,
- WebAssembly 星系
- ASM.js星系(当前这一集)
- C 星系
- PHP星系,以及
- NodeJS 星系
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程序,如果有优化,那么你就可以得到不错的性能提升。
这个图片展示了3个基准测试,分别对于不同的Javascript引擎:Firefox, Firefox+asm.js, Google, 和Native。
记住ASM.js是被设计为一种编译目标。因此一般你不需要特别关心,因为那是编译器的活。对把C或者C++编译到Web的一个典型的编译和执行流程如下
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到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二进制一样,我们也可以gzip和brotli压缩输出文件:
$ # 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输出是什么样的?
我可以说非常的喜欢它!
#ASM.js ? Javascript
输出的gutenberg_post_parser.asm.js
文件包含一个唯一的函数叫做:GUTENBERG_POST_PARSER_ASM_MODULE
,它返回一个对象包含了4个私有函数。
- root,语法根
- alloc,来分配内存
- dealloc,释放内存
- 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越多的往后旅行,也会变得更加有趣。
谢谢阅读!