离开节点之海 • V8

V8 的末端优化编译器 Turbofan 曾使用Sea of Nodes(SoN),但近 3 年已开始摆脱 SoN 并回归更传统的Control-Flow Graph(CFG)中间表示(IR),名为 Turboshaft。如今 Turbofan 的整个 JavaScript 后端及 WebAssembly 都使用 Turboshaft,仅内置管道和 JavaScript 管道前端仍使用部分 SoN。

The birth of Turbofan and Sea of Nodes

12 年前的 2013 年,V8 有单一优化编译器 Crankshaft,使用基于 CFG 的中间表示,虽提供显著性能提升但存在诸多问题,如手写大量汇编代码、难以优化 asm.js、不允许在降低过程中引入控制流、不支持 try-catch、有很多性能悬崖和退出、包含许多去优化循环等。于是决定用新编译器 Turbofan 替代 Crankshaft,Turbofan 使用 Sea of Nodes 这一据称更强大的 IR,当时该 IR 已在 Java HotSpot 虚拟机的 JIT 编译器 C2 中使用 10 多年。

But what is Sea of Nodes, really?

CFG 以基本块为节点、边表示控制流来表示程序,而 Sea of Nodes 以单个指令为节点、边表示值使用和控制流来表示程序。它能更好地处理指令之间的真实依赖,但也带来一些问题,如离编译器输入和输出较远,使理解和编写降低过程更困难。

And the troubles begin…

  1. Manually/visually inspecting and understanding a Sea of Nodes graph is hard:小程序中 CFG 更易读,对编译器工程师来说,查看 Turbofan 图理解 bug 或找优化机会不易,而 CFG 更易匹配元素到源代码或生成的汇编。
  2. Too many nodes are on the effect chain and/or have a control input:典型 JavaScript 图中很多节点在效果链或有控制输入,如字符串拼接函数的 Turbofan 图,很多检查和加载操作在效果链上,违背了 Sea of Nodes 的初衷。
  3. Memory operations do not float easily:在 SoN 中,内存操作不易自由浮动,如简单的 if-else 语句中数组元素的加载操作,SoN 不能像预期那样让它们自由下浮,实际上 SoN 中纯节点才自由浮动,效果链常像 CFG 中的控制链。
  4. Managing the effect and control chains manually is hard:由于 JavaScript 中很多节点有副作用和分支,需同时跟踪效果链和控制链,这容易出错且难以阅读和维护,如代码中的多个效果链管理示例。
  5. The scheduler is too complex:指令调度理论简单,但 SoN 中存在冗余消除和指令重复问题,如简单的 switch-case 示例,这使调度器更复杂,需要额外逻辑来处理。
  6. Finding a good order to visit the graph is difficult:CFG 中访问节点顺序简单,而 SoN 中纯指令无控制或效果链,通常从末尾开始访问,这不是好的访问顺序,导致节点平均每 20 次访问才改变一次,状态跟踪困难且昂贵,如 Turbofan 的加载消除阶段在大图上有退出。
  7. Cache unfriendliness:Turbofan 各阶段在原地修改图,节点较大且多次降低会引入新节点,导致图的缓存不友好,SoN 比新的 CFG IR 平均多约 3 倍的 L1 dcache 缺失,在某些阶段最多多 7 倍,估计耗费约 5%的编译时间。
  8. Control-flow dependent typing is limited:如一个简单的 JavaScript 函数示例,在 CFG 中可轻松优化检查操作,而在 SoN 中纯操作会自由流动,无法移除检查,直到计算调度后才回到 CFG,这对生成的代码质量有影响。
  9. And many other issues…:还有传播死代码困难、难以引入新控制流、难以确定循环内部内容、编译慢、破坏先前调度等问题。

总之,Sea of Nodes 虽优雅但对 JavaScript 来说不实用,存在太复杂、太局限、编译慢等问题,V8 最终决定回归传统 CFG IR Turboshaft,新 IR 带来诸多好处,如编译时间减半、代码更简单等。

阅读 6
0 条评论