Node.js 底层解析 #6 - 老式 V8 引擎
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
在上一篇文章中,我们讨论了变量分配、隐藏类以及 V8 如何处理 JavaScript 代码。现在,我们将更深入地了解编译流程以及 V8 的各个组件。
在 2017 年 V8.5.9 版本发布之前,V8 使用的是一套旧的执行流程,它由一个完整的代码生成编译器和一个名为 Crankshaft 的 JIT 编译器组成,Crankshaft 又包含两个子组件:Hydrogen 和 Lithium。Mathias Bynens 提供的这张图很好地展示了我们之前的流程:
我们来简单谈谈它们。
完整的代码生成编译器
全代码生成编译器是一个简单且速度极快的编译器,它生成的是简单但速度相对较慢(未经优化)的机器代码。该编译器的主要目的是追求极致速度,但生成极其糟糕的代码。因此,它能以光速将 JavaScript 代码翻译成机器代码,但生成的代码未经优化,运行速度可能非常慢。此外,它还会处理类型反馈,在程序运行时收集有关数据类型和函数使用情况的信息。
它首先读取我们的抽象语法树(AST),遍历所有节点,并直接向宏汇编器发出调用。结果:生成通用的本地代码。就是这样!完整的代码生成器完成了它的任务。所有复杂情况都通过向运行时过程发出调用来处理,所有局部变量都像往常一样存储在堆上。当 V8 识别出热函数和冷函数时,神奇的事情就开始了!
热点函数是指在程序执行过程中被多次调用的函数,因此需要比其他函数进行更多优化。冷门函数则恰恰相反。这时就需要用到 Crankshaft 编译功能了。
曲轴
Crankshaft 编译器曾经是默认的 JIT 编译器,负责处理 JS 的所有优化部分。
在接收到运行时生成的类型信息和调用信息后,Crankshaft 会分析数据,找出哪些函数已成为热点。然后,Crankshaft 会遍历抽象语法树 (AST),为这些特定函数生成优化代码。之后,优化后的函数将使用所谓的栈上替换 (OSR) 技术替换未优化的函数。
但是,这个优化后的函数并不能涵盖所有情况,因为它只针对执行过程中我们传递的那些已定义的类型进行了优化。让我们想象一下我们的readFile函数。在函数的开头几行,我们有:
const readFileAsync = (filePath) => { /* ... */ }
假设这个函数很热门,filePath并且返回一个字符串,那么 Crankshaft 会对其进行优化以使其适用于字符串。但现在,假设返回值filePath是空字符串null,或者是一个数字(谁知道呢?)。那么优化后的函数就不适用于这种情况了。因此,Crankshaft 会取消优化,用原始函数替换它。
为了解释这种神奇的运作原理,我们需要了解曲轴内部的一些部件。
氢编译器
Hydrogen 编译器以带有类型反馈信息的抽象语法树 (AST) 作为输入。基于这些信息,它生成所谓的高级中间表示 (HIR),该表示具有静态单赋值 (SSA) 形式的控制流图 (CFG),其形式如下:
对于以下给定函数:
function clamp (x, lower, upper) {
if (x < lower) x = lower
else if (x > upper) x = upper
return x
}
SSA 的翻译是:
entry:
x0, lower0, upper0 = args;
goto b0;
b0:
t0 = x0 < lower0;
goto t0 ? b1 : b2;
b1:
x1 = lower0;
goto exit;
b2:
t1 = x0 > upper0;
goto t1 ? b3 : exit;
b3:
x2 = upper0;
goto exit;
exit:
x4 = phi(x0, x1, x2);
return x4;
在 SSA 中,变量不会重复赋值;它们只绑定一次,即被赋予一个值。这种形式将任何过程分解为若干个基本计算块,每个计算块都以跳转到另一个计算块结束,无论该跳转是否基于条件。如您所见,变量在每次赋值时都绑定到一个唯一的名称,最后,函数phi会将所有x赋值后的变量合并在一起,并返回具有值的那个变量。
在生成 HIR 时,Hydrogen 会对代码应用多种优化,例如常量折叠、方法内联以及我们将在本指南末尾看到的其他内容——这部分内容有专门的章节介绍。
Hydrogen 输出的结果是一个优化的 CFG,下一个编译器 Lithium 将其作为输入来生成实际的优化代码。
锂编译器
正如我们所说,Lithium 编译器会将 HIR 转换为特定于机器的底层中间表示 (LIR)。LIR 在概念上类似于机器代码,但它是平台无关的。
在生成此 LIR 的同时,应用了新的代码优化,但这次是低级优化。
最后,读取此 LIR,Crankshaft 为每个 Lithium 指令生成一系列本地指令,应用 OSR,然后执行代码。
结论
这是我们讨论V8编译流程系列文章的第一部分(共两部分)。敬请期待本系列的下一篇文章!
文章来源:https://dev.to/_staticvoid/node-js-under-the-hood-6-the-old-v8-34hm如果您发现本文有任何拼写错误、语法错误或其他问题,请告诉我!所有反馈对我都非常重要,有助于我提升文章质量!<3