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
- 在 V8 中,设置
--liftoff --no-wasm-tier-up
标志。 - 在 Chrome 中,禁用 WebAssembly 分层(
chrome://flags/#enable-webassembly-tiering
)并启用 WebAssembly 基线编译器(chrome://flags/#enable-webassembly-baseline
)。
- 在 V8 中,设置
仅 TurboFan
- 在 V8 中,设置
--no-liftoff --no-wasm-tier-up
标志。 - 在 Chrome 中,禁用 WebAssembly 分层(
chrome://flags/#enable-webassembly-tiering
)并禁用 WebAssembly 基线编译器(chrome://flags/#enable-webassembly-baseline
)。
- 在 V8 中,设置
延迟编译
- 延迟编译是一种编译模式,其中函数仅在第一次被调用时才被编译。与生产配置类似,该函数首先使用 Liftoff 编译(阻塞执行)。Liftoff 编译完成后,该函数将在后台使用 TurboFan 重新编译。
- 在 V8 中,设置
--wasm-lazy-compilation
标志。 - 在 Chrome 中,启用 WebAssembly 延迟编译(
chrome://flags/#enable-webassembly-lazy-compilation
)。
编译时间 #
有不同的方法可以衡量 Liftoff 和 TurboFan 的编译时间。在 V8 的生产配置中,Liftoff 的编译时间可以通过 JavaScript 测量,方法是测量 new WebAssembly.Module()
完成所需的时间,或者测量 WebAssembly.compile()
解析 Promise 所需的时间。要测量 TurboFan 的编译时间,可以在仅 TurboFan 的配置中执行相同的操作。
还可以通过在 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
类别可以获得更详细的跟踪数据,该类别除了其他信息外,还提供单个函数的编译时间。