使WebAssembly更快:Firefox最新的流式分层编译器

很多人认为WebAssembly是一种可能改变游戏规则的技术,因为它可以使代码在Web上运行得更快。目前已经有了一些 WebAssembly加速技术,还有一些加速方案即将出现。

其中一种加速技术就是流式编译,即允许代码一边下载,浏览器一边编译。直到现在(原作者写此文时,Firxfox 58还未发布),流式编译还是一种潜在的加速技术,随着Firefox 58版本的发布,这种技术变成了现实。

Firefox 58中的另一种新的机制是引入了新的两层编译器,即baseline编译器和优化编译器。其中baseline编译器的代码编译速度比优化编译器快10-15倍。

流式编译和分层编译器结合起来意味着代码编译的速度比从网络中下载还要快。

这里写图片描述

在PC机上,编译WebAssembly的速度可以达到30-60兆字节/秒,这比在网络中传送数据包的速度还要快。

使用Firefox Nightly或Beta版本,你可以在自己的设备上进行一下测试,即使是在平均性能的移动设备上,编译速度也可以达到8兆字节/秒——这几乎比所有移动网络的平均下载速度还要快。

为什么这很重要

Web性能倡导者认为当页面中包含大量JavaScript时,JavaScript的下载会导致页面的加载速度变慢。

这主要是因为解析和编译时间。Steve Souders指出,Web性能曾经的瓶颈在于网络,而如今Web性能的新瓶颈是CPU以及主线程的执行速度。

这里写图片描述

因此我们希望尽可能地将更多的工作从主线程分离出来,而且越早越好以便充分利用所有的CPU时间,更进一步,希望CPU做更少的工作。

使用JavaScript,可以做到一些,比如当数据包下载的过程中,可以在其他线程中解析JS,解析操作不可避免且有较大的工作量,另外需要等待解析完成才能开始编译。为了编译,又得切回到主线程,因为JavaScript往往是在运行时编译。

这里写图片描述

使用WebAssembly所需要做的工作就少了,WebAssembly解码远比解析JS简单而且速度更快,而且解码和编译工作可以拆分到多个线程。

这意味着baseline编译工作是在多个线程中并行完成的,这样速度将更快,一旦完成编译,编译生成的代码便可以在主线程上立即执行,就像上面JS一样,不需要因为编译而暂停。

这里写图片描述

当baseline编译生成的代码在主线程上运行时,其他线程仍然在工作,它们将生成一个更加优化的版本。当这个优化版本完成之后将被交换到主线程,因此代码会执行的更快。

加载WebAssembly的开销更像是解码一张图片而不是加载JavaScript。想想看,那些Web性能倡导者们对加载150kB的JS脚本感到非常恐惧,而对于加载一张150kB的图片却认为合情合理。

这里写图片描述

那是因为图片的加载时间非常快,Addy OSmani在 The Cost of JavaScript 中做过解释,而且图片解码并不会阻塞主线程,Alex Russell在其文章 Can You Afford It?: Real-world Web Performance Budgets 中也讨论过。

但这并不意味着我们希望WebAssembly文件像图片文件那样大。虽然早期的WebAssembly工具创建的文件比较大,那是因为包含了很多运行时内容,现在有很多方式可以将WebAssembly文件变得更小。例如 Emscripten has a “shrinking initiative”. In Rust, you can already get pretty small file sizes using the wasm32-unknown-unknown target。而且还有类似 wasm-gcwasm-snip 这样进一步优化的工具。

这意味着WebAssembly文件比等效的JavaScript加载更快。

这就厉害了!正如Yehuda Katz所说,WebAssembly将是游戏规则的改变者。

这里写图片描述

接下来看一下新的编译器是如何工作的。

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

如果更早地开始编译代码,则将更早地完成编译,这正是流式编译器做的事情,尽可能早地编译.wasm文件。

当下载一个文件时,并不会整个文件一起下载,而是被分割成一系列数据包下载。

当.wasm文件中的每一个数据包下载完成时,浏览器的网络层会将它放进一个ArrayBuffer中。

这里写图片描述

然后,将会把ArrayBuffer交给Web虚拟机(即JS引擎),WebAssembly编译器便开始编译。

这里写图片描述

其实没有理由让编译器等待,像上面这样逐块编译WebAssembly在技术上是可行的,这意味着当一块数据到达时就开始编译。

这就是我们新的编译器所做的事情,它利用了WebAssembly的流式API。

这里写图片描述

如果给 WebAssembly.instantiateStreaming一个响应对象,那么当第一个数据块到达时就会进入WebAssembly引擎,然后编译器就开始处理第一块数据,而下一个数据块则仍在下载中。

这里写图片描述

除了支持下载与编译的并行之外,还有另一个优点。

.wasm模块的code部分位于data部分之前,因此利用流式方法,编译器可以在data部分下载时编译code部分。如果data部分非常大,可能以兆字节计,这将非常有意义。

这里写图片描述

使用流式编译,尽管很早就开始启动编译,但也可以让编译速度更快。

第一层 baseline编译器:更快地编译代码

如果想让代码运行地更快,则需要对它进行优化,但是当编译代码时执行优化操作将会耗费额外时间,同时也会降低编译速度,所以这需要权衡。

如果既要高效编译也要进行优化,可以使用两个编译器,一个编译器负责快速编译代码而不做太多优化,另一个编译器的编译速度相对较慢但是最终会生成更加优化的代码。

这就是所谓的分层编译器。当代码进来时,首先被第一层(baseline)编译器编译,当baseline编译器开始启动编译之后,第二层编译器(优化编译器)在后台也将再次编译代码,最终生成一个优化的版本。

第二层编译完成后,执行一次热替换,将之前baseline编译器生成的版本替换为优化后的版本,这将大大提升代码的执行速度。

这里写图片描述

虽然JavaScript引擎很早之前就在使用分层编译,但是JS引擎仅仅对某一小部分经常被调用的代码才使用优化编译器对其进行优化。

而WebAssembly中的优化编译器会对全部代码进行重编译,会优化模块中的所有代码。未来,我们可能会添加一些选项让开发者来控制是否要延迟优化。

baseline编译器在启动时节省了大量时间,它比第二层优化编译器的编译速度快10-15倍,而它产生的代码在我们的测试中,仅比优化过的代码慢2倍。

这意味着即使运行由baseline编译器生成的未优化的代码时,运行速度也会非常快。

并行:让这一切更快

article on Firefox Quantum 这篇文章中,我曾介绍过粗粒度和细粒度的并行操作,我们使用这两种方式来编译WebAssembly。

上文提到优化编译器会在后台线程编译,这意味着主线程可用来执行代码。由baseline编译器生成的代码在主线程运行时,后台线程的优化编译器会对代码进行重编译优化。

但是在很多计算机上仍然有多个处理器没有利用,为了充分利用多核计算能力,两个编译器均使用细粒度模式分解工作并行执行。

并行执行的单元是函数,每个函数可以在不同的处理器核心上独立编译。事实上,我们可以把这些函数批量化为不同的函数组,然后将这些函数组发送到不同的处理器核心上。

…使用缓存跳过这些工作(未来计划)

当前,每次加载页面时都会重新解码和编译,但是如果拥有相同的.wasm文件,最终都会得到相同的机器码。

这意味着大部分时间,这些工作可以跳过,这正是我们未来计划要做的事情。未来仅会在第一次加载页面时解码和编译,然后在HTTP缓存中缓存上述生成的机器码,这样当再次请求同一个URL时,则直接拉取缓存中的机器码。

这将使得后续进入缓存过的页面时根本不耗费什么时间。

这里写图片描述

我们已经为未来打好了基础,在Firefox 58版本中已经像上面提到那样 缓存了JavaScript字节码,后面只需要扩展并支持缓存.wasm文件的机器码即可。

原文作者:Lin Clark

Lin is an engineer on the Mozilla Developer Relations team. She tinkers with JavaScript, WebAssembly, Rust, and Servo, and also draws code cartoons.

原文地址: Making WebAssembly even faster: Firefox’s new streaming and tiering compiler

猜你喜欢

转载自blog.csdn.net/u014738140/article/details/79177572
今日推荐