WebAssembly 编译流程

WebAssembly 是一种二进制格式,允许您在 Web 上高效安全地运行来自 JavaScript 以外的编程语言的代码。本文档深入探讨了 V8 中的 WebAssembly 编译流程,并解释了我们如何使用不同的编译器来提供良好的性能。

Liftoff #

最初,V8 不会编译 WebAssembly 模块中的任何函数。相反,函数在第一次被调用时会使用基线编译器 Liftoff 延迟编译。Liftoff 是一个 单遍编译器,这意味着它只遍历一次 WebAssembly 代码,并立即为每个 WebAssembly 指令生成机器代码。单遍编译器擅长快速代码生成,但只能应用一小部分优化。实际上,Liftoff 可以非常快地编译 WebAssembly 代码,每秒数十兆字节。

Liftoff 编译完成后,生成的机器代码将与 WebAssembly 模块注册,以便在将来调用该函数时可以立即使用编译后的代码。

TurboFan #

Liftoff 在很短的时间内生成了相当快的机器代码。但是,由于它独立地为每个 WebAssembly 指令生成代码,因此几乎没有优化空间,例如改进寄存器分配或常见的编译器优化,例如冗余加载消除、强度削弱或函数内联。

这就是为什么函数(经常执行的函数)会使用 TurboFan(V8 中用于 WebAssembly 和 JavaScript 的优化编译器)重新编译的原因。TurboFan 是一个 多遍编译器,这意味着它在生成机器代码之前会构建编译代码的多个内部表示。这些额外的内部表示允许优化和更好的寄存器分配,从而产生明显更快的代码。

V8 监控 WebAssembly 函数被调用的频率。一旦函数达到某个阈值,该函数就被认为是的,并且重新编译将在后台线程上触发。编译完成后,新代码将与 WebAssembly 模块注册,替换现有的 Liftoff 代码。任何对该函数的新调用将使用 TurboFan 生成的新的优化代码,而不是 Liftoff 代码。但请注意,我们不会进行栈上替换。这意味着如果 TurboFan 代码在函数被调用后可用,函数调用将使用 Liftoff 代码完成执行。

代码缓存 #

如果 WebAssembly 模块是用 WebAssembly.compileStreaming 编译的,那么 TurboFan 生成的机器代码也将被缓存。当从同一个 URL 再次获取同一个 WebAssembly 模块时,可以立即使用缓存的代码,无需额外编译。有关代码缓存的更多信息,请参阅 单独的博客文章

只要生成的 TurboFan 代码量达到某个阈值,就会触发代码缓存。这意味着对于大型 WebAssembly 模块,TurboFan 代码会增量缓存,而对于小型 WebAssembly 模块,TurboFan 代码可能永远不会被缓存。Liftoff 代码不会被缓存,因为 Liftoff 编译几乎与从缓存加载代码一样快。

调试 #

如前所述,TurboFan 应用优化,其中许多优化涉及重新排序代码、消除变量甚至跳过整个代码段。这意味着如果您想在特定指令处设置断点,可能不清楚程序执行应该实际停止的位置。换句话说,TurboFan 代码不适合调试。因此,当通过打开 DevTools 启动调试时,所有 TurboFan 代码将再次被 Liftoff 代码替换(“降级”),因为每个 WebAssembly 指令都映射到机器代码的单个部分,并且所有局部和全局变量都保持不变。

性能分析 #

为了使事情更加混乱,在 DevTools 中,当打开“性能”选项卡并单击“记录”按钮时,所有代码将再次升级(使用 TurboFan 重新编译)。“记录”按钮启动性能分析。分析 Liftoff 代码将不具有代表性,因为它仅在 TurboFan 未完成时使用,并且可能比 TurboFan 的输出慢得多,而 TurboFan 的输出将在大多数时间运行。

实验标志 #

为了进行实验,V8 和 Chrome 可以配置为仅使用 Liftoff 或仅使用 TurboFan 编译 WebAssembly 代码。甚至可以尝试延迟编译,其中函数仅在第一次被调用时才被编译。以下标志启用这些实验模式

编译时间 #

有不同的方法可以衡量 Liftoff 和 TurboFan 的编译时间。在 V8 的生产配置中,Liftoff 的编译时间可以通过 JavaScript 测量,方法是测量 new WebAssembly.Module() 完成所需的时间,或者测量 WebAssembly.compile() 解析 Promise 所需的时间。要测量 TurboFan 的编译时间,可以在仅 TurboFan 的配置中执行相同的操作。

Google Earth 中 WebAssembly 编译的跟踪。

还可以通过在 chrome://tracing/ 中启用 v8.wasm 类别来更详细地测量编译。Liftoff 编译是从开始编译到 wasm.BaselineFinished 事件所花费的时间,TurboFan 编译在 wasm.TopTierFinished 事件结束。编译本身从 WebAssembly.compileStreaming()wasm.StartStreamingCompilation 事件开始,从 new WebAssembly.Module()wasm.SyncCompile 事件开始,以及从 WebAssembly.compile()wasm.AsyncCompile 事件开始,分别。Liftoff 编译用 wasm.BaselineCompilation 事件表示,TurboFan 编译用 wasm.TopTierCompilation 事件表示。上图显示了为 Google Earth 记录的跟踪,突出显示了关键事件。

使用 v8.wasm.detailed 类别可以获得更详细的跟踪数据,该类别除了其他信息外,还提供单个函数的编译时间。