[译] 使 WebAssembly 更快:Firefox 的新流式和分层编译器

人们都说 WebAssembly 是一个游戏规则改变者,由于它可让代码更快地在网络上运行。有些加速已经存在,还有些在不远的未来。javascript

其中一种加速是流式编译,即浏览器在代码还在下载的时候就对其进行编译。截至目前,这只是潜在的将来加速(方式)。但随着下周 Firefox 58 版本的发布,它将成为现实。前端

Firefox 58 还包含两层新的编译器。新的基线编译器编译代码的速度比优化编译器快了 10-15 倍。java

综合起来,这两个变化意味着咱们编译代码的速度比从网络中编译代码速度快。android

在台式电脑上,咱们每秒编译 30-60 兆字节的 WebAssembly 代码。这比网络传送数据包的速度还快。ios

若是你使用 Firefox Nightly 或者 Beta,你能够在你本身设备上试一试。即使是在很普通的移动设备上,咱们能够每秒编译 8 兆字节 —— 这比任何移动网络的平均下载速度都要快得多。git

这意味着你的代码几乎是在它完成下载后就当即执行。github

为何这很重要?

当网站发布大批量 JavaScript 代码时,Web 性能拥护者会变得一筹莫展。这是由于下载大量的 JavaScript 会让页面加载变慢。web

这很大程度是由于解析和编译时间。正如 Steve Souder 指出,网络性能的旧瓶颈曾是网络。但如今网络性能的新瓶颈是 CPU,特别是主线程。后端

Old bottleneck, the network, on the left. New bottleneck, work on the CPU such as compiling, on the right

因此咱们想要尽量多的把工做从主线程中移除。咱们也想要尽量早的启动它,以便咱们充分利用 CPU 的全部时间。更好的是,咱们能够彻底减小 CPU 工做量。api

使用 JavaScript 时,你能够作一些这样的事情。你能够经过流入的方式在主线程外解析文件。但你仍是须要解析它们,这就须要不少工做,而且你必须等到它们都解析完了才能开始编译。而后编译的时候,你又回到了主线程上。这是由于 JS 一般是运行时延迟编译的。

Timeline showing packets coming in on the main thread, then parsing happening simultaneously on another thread. Once parse is done, execution begins on main thread, interrupted occassionally by compiling

使用 WebAssembly,启动的工做量减小了。解码 WebAssembly 比解析 JavaScript 更简单,更快捷。而且这些解码和编译能够跨多个线程进行拆分。

这意味着多个线程将运行基线编译,这会让它变得更快。一旦完成,基线编译好的代码就能够在主线程上开始执行。它没必要像 JS 代码同样暂停编译。

Timeline showing packets coming in on the main thread, and decoding and baseline compiling happening across multiple threads simultaneously, resulting in execution starting faster and without compiling breaks.

当基线编译的代码在主线程上运行时,其余线程则在作更优化的版本。当更优化的版本完成时,它就会替换进来使得代码运行更加快捷。

这使得加载 WebAssembly 的成本变得更像解码图片而不是加载 JavaScript。而且想一想看 —— 网络性能倡导者确定接受不了 150kB 的 JS 代码负载量,但相同大小的图像负载量并不会引发人们的注意。

Developer advocate on the left tsk tsk-ing about large JS file. Developer advocate on the right shrugging about large image.

这是由于图像的加载时间要快得多,就像 Addy Osmani 在 JavaScript 的成本 中解释的那样,解码图像并不会阻塞主线程,正如 Alex Russell 在你能接受吗?真实的 Web 性能预算中所讨论的那样。

但这并不意味着咱们但愿 WebAssembly 文件和图像文件同样大。虽然早期的 WebAssembly 工具建立了大型的文件,是由于它们包含了不少运行时(内容),目前来看还有不少工做要作让文件变得更小。例如,Emscripten 有一个“缩小协议”。在 Rust 中,你已经能够经过使用 wasm32-unknown-unknown 目标来获取至关小尺寸的文件,而且还有像 wasm-gcwasm-snip 这样的工具来帮助进一步优化它们。

这就意味着这些 WebAssembly 文件的加载速度要比等量的 JavaScript 快得多。

这很关键。正如 Yehuda Katz 指出,这是一个游戏规则改变者。

Tweet from Yehuda Katz saying it's possible to parse and compile wasm as fast as it comes over the network.

因此让咱们看看新编译器是怎么工做的吧。

流式编译:更早开始的编译

若是你更早开始编译代码,你就更早完成它。这就是流式编译所作的 —— 尽量快地开始编译 .wasm 文件。

当你下载文件时,它不是单件式的。实际上,它带来的是一系列数据包。

以前,当 .wasm 文件中的每一个包正在下载时,浏览器网络层会把它放进 ArrayBuffer(译者注:数组缓存)中。

Packets coming in to network layer and being added to an ArrayBuffer

而后,一旦完成下载,它会将 ArrayBuffer 转移到 Web VM(也就是 JS 引擎)中。也就到了 WebAssembly 编译器要开始编译的时候。

Network layer pushing array buffer over to compiler

可是没有充分的理由让编译器等待。从技术上讲,逐行编译 WebAssembly 是可行的。这意味着你可以在第一个块进来的时候就开始启动。

因此这就是咱们新编译器所作的。它利用了 WebAssembly 的流式 API。

WebAssembly.instantiateStreaming call, which takes a response object with the source file. This has to be served using MIME type application/wasm.

若是你提供给 WebAssembly.instantiateStreaming 一个响应的对象,则(对象)块一旦到达就会当即进入 WebAssembly 引擎。而后编译器能够开始处理第一个块,即使下一个块还在下载中。

Packets going directly to compiler

除了可以并行下载和编译代码外,它还有另一个优点。

.wasm 模块中的代码部分位于任何数据(它将引入到模块的内存对象)以前。所以,经过流式传输,编译器能够在模块的数据仍在下载的时候就对其进行编译。若是当你的模块须要大量的数据,且多是兆字节的时候,这些就会显得很重要。

File split between small code section at the top, and larger data section at the bottom

经过流式传输,咱们能够提早开始编译。并且咱们一样能够更快速地进行编译。

第 1 层基线编译器:更快的编译代码

若是你想要代码跑的快,你就须要优化它。可是当你编译时执行这些优化会花费时间,也就会让编译代码变得更慢。因此这里须要一个权衡。

但鱼和熊掌能够兼得。若是咱们使用两个编译器,就能让其中一个快速编译可是不作过多的优化工做,而另外一个虽然编译慢,可是建立了更多优化的代码。

这就称做为层编译器。当代码第一次进入时,将由第 1 层(或基线)编译器对其编译。而后,当基线编译完成,代码开始运行以后,第 2 层编译器再一次遍历代码并在后台编译更优化的版本。

一旦它(译者注:第 2 层编译)完成,它会将优化后的代码热插拔为先前的基线版本。这使代码执行得更快。

Timeline showing optimizing compiling happening in the background.

JavaScript 引擎已经使用分层编译器很长一段时间了。然而,JS 引擎只在一些代码变得“温热” —— 当代码的那部分被调用太屡次时,才会使用第 2 层(或优化)编译器。

相比之下,WebAssembly 的第 2 层编译器会热切地进行全面的从新编译,优化模块中的全部代码。在将来,咱们可能会为开发者添加更多选项,用来控制如何进行激进的优化或者惰性的优化。

基线编译器在启动时节省了大量时间。它编译代码的速度比优化编译器的快 10-15 倍。而且在咱们的测试中,它建立代码的速度只慢了 2 倍。

这意味着,只要仍在运行基线编译代码,即使是在最开始的几分钟你的代码也会运行地很快。

并行化:让一切更快

关于 Firefox Quantum 的文章中,我解释了粗粒度和细粒度的并行化。咱们能够用它们来编译 WebAssembly。

我在上文有提到,优化编译器会在后台进行编译。这意味着它空出的主线程可用于执行代码。基线编译版本的代码能够在优化编译器进行从新编译时运行。

但在大多数电脑上仍然会有多个核心没有使用。为了充分使用全部核心,两个编译器都使用细粒度并行化来拆解工做。

并行化的单位是功能,每一个功能均可以在不一样的核心上单独编译。这就是所谓的细粒度,实际上,咱们须要将这些功能分批处理成更大的功能组。这些批次会被派送到不一样的核内心。

...而后经过隐式缓存彻底跳过全部工做(将来的任务)

目前,每次从新加载页面时都会重作解码和编译。可是若是你有相同的 .wasm 文件,它编译后都是同样的机器代码。

这意味着,不少时候这些工做均可以跳过。这些也是将来咱们要作的。咱们将在第一页加载时进行解码和编译,而后将生成的机器码缓存在 HTTP 缓存中。以后当你再次请求这个 URL 的时候,它会拉取预编译的机器代码。

这就能让后续加载页面的加载时间消失了。

Timeline showing all work disappearing with caching.

这项功能已经有了基础构建。咱们在 Firefox 58 版本中缓存了这样的 JavaScript 字节代码。咱们只需扩展这种支持来缓存 .wasm 文件的机器代码。

关于 Lin Clark

Lin 是 Mozilla Developer Relations 团队的工程师。她致力于 JavaScript、WebAssembly、Rust 和 Servo,还会绘制代码漫画。

Lin Clark 的更多文章...

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索