面试写:说说执行 JavaScript 的 V8 引擎做了什么?

开发 前端
V8 引擎是由 Google 用 C++ 开源的 JavaScript 与 WebAssembly 引擎,目前像是 Chrome 和 Node.js 都是使用 V8 在执行 JavaScript。除了 V8 以外还有 SpiderMonkey(最早的 JavaScript 引擎,目前是 Firefox 浏览器在使用)与 JavaScriptCore(Safari 浏览器使用)等其他 JavaScr

Hi!大家好,我想点进来的大家应该都听过,也在浏览器或 Node.js 上执行过 JavaScript,但你们有想过 JavaScript 是如何执行的吗?这背后的功臣就是 JavaScript 引擎,而标题提到的 V8 引擎 也是其中之一哟!

V8 引擎是由 Google 用 C++ 开源的 JavaScript 与 WebAssembly 引擎,目前像是 Chrome 和 Node.js 都是使用 V8 在执行 JavaScript。除了 V8 以外还有 SpiderMonkey(最早的 JavaScript 引擎,目前是 Firefox 浏览器在使用)与 JavaScriptCore(Safari 浏览器使用)等其他 JavaScript 引擎。

好的,那麽 V8 引擎到底是如何执行 JavaScript 的呢?

V8 引擎执行流程

Scanner

V8 引擎取得 JavaScript 源代码后的第一步,就是让 Parser 使用 Scanner​ 提供的 Tokens(Tokens 裡有 JavaScript 内的语法关键字,像是 function、async、if 等),将 JavaScript 的原始码解析成** abstract syntax tree**,就是大家常在相关文章中看到的 AST(抽象语法树)。

如果好奇 AST 长什麽样子的话,可以使用 acron 这个 JavaScript Parser,或是 这个网站 生成 AST 参考看看。以下是使用 acron 的代码:

const { Parser } = require('acorn')

const javascriptCode = `
let name;
name = 'Clark';
`;

const ast = Parser.parse(javascriptCode, { ecmaVersion: 2020 });
console.log(JSON.stringify(ast));

下方是解析 let name; name = 'Clark'; 所得到的 AST:

{
"type": "Program",
"start": 0,
"end": 31,
"body": [
{
"type": "VariableDeclaration",
"start": 3,
"end": 12,
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 11,
"id": {
"type": "Identifier",
"start": 7,
"end": 11,
"name": "name"
},
"init": null
}
],
"kind": "let"
},
{
"type": "ExpressionStatement",
"start": 15,
"end": 30,
"expression": {
"type": "AssignmentExpression",
"start": 15,
"end": 29,
"operator": "=",
"left": {
"type": "Identifier",
"start": 15,
"end": 19,
"name": "name"
},
"right": {
"type": "Literal",
"start": 22,
"end": 29,
"value": "Clark",
"raw": "'Clark'"
}
}
}
],
"sourceType": "script"
}

如果再进一步,将上方的 AST 转化成图表,会长这样:

图片

AST 可以从上到下,由左而右去理解它在执行的步骤:

  • 走 VariableDeclaration 建立名字为name 的变量。
  • 走ExpressionStatement 到表达式。
  • 走AssignmentExpression​ 遇到=​,且左边为name​,右边为字串Clark。

产生 AST 后,就完成了 V8 引擎的第一个步骤。

JIT(Just-In-Time)

JIT 的中文名称是即时编译,这也是 V8 引擎所採用在执行时编译 JavaScript 的方式。

将代码转变为可执行的语言有几种方法,第一种是编译语言,像是 C/C++ 在写完代码的时候,会先经过编译器将代码变成机器码才能执行。第二种就像 JavaScript,会在执行的时候将代码解释成机器懂的语言,一边解释边执行的这种,称作直译语言。

编译语言的好处是可以在执行前的编译阶段,审视所有的代码,将可以做的优化都完成,但直译语言就无法做到这一点,因为执行时才开始解释的关係,执行上就相对较慢,也没办法在一开始做优化,为了处理这个状况,JIT 出现了。

JIT 结合解释和编译两者,让执行 JavaScript 的时候,能够分析代码执行过程的情报,并在取得足够情报时,将相关的代码再编译成效能更快的机器码。

听起来 JIT 超讚,而在 V8 引擎裡负责处理 JIT 的左右手分别为 Ignition 和 **TurboFan**。

Ignition & TurboFan

成功解析出 AST 后,Ignition​ 会将 AST​ 解释为 ByteCode​,成为可执行的语言,但是 V8 引擎还未在这裡结束,Ignition 用 ByteCode 执行的时候,会搜集代码在执行时的类型信息。举个例子,如果我们有个 sum​ 函式,并且始终确定呼叫的参数类型都是 number,那麽 Ignition 会将它记录起来。

此时,在另一方面的 TurboFan 就会去查看这些信息,当它确认到“只有 number​ 类型的参数会被送进 sum​ 这个函式执行”这个情报的时候,就会进行 Optimization,把 sum 从 ByteCode 再编译为更快的机器码执行。

如此一来,就能够保留 JavaScript 直译语言的特性,又能够在执行的时候优化性能。

但毕竟是 JavaScript,谁也不敢保证第一百万零一次送进来的参数仍然是 number​,因此当 sum 接收到的参数与之前 Optimization 的策略不同时,就会进行 Deoptimization 的动作。

TurboFan 的 Optimization 并不是将原有的 ByteCode 直接变成机器码,而是在产生机器码的同时,增加一个 Checkpoint 到 ByteCode 和机器码之间,在执行机器码之前,会先用 Checkpoint 检查是否与先前 Optimization 的类型符合。这样的话,当 sum 以与 Optimization 不同的类型被呼叫的时候,就会在 Checkpoint 这关被挡下来,并进行 Deoptimization。

最后如果 TurboFan 重複执行了 5 次 Optimization 和 Deoptimization 的过程,就会直接放弃治疗,不会再帮这个函式做 Optimization。

那到底该怎麽知道 TurboFan 有没有真的做 Optimization 咧?我们可以用下方的代码来做个实验:

const loopCount = 10000000;
const sum = (a, b) => a + b;

performance.mark('first_start');

for (let i = 0; i < loopCount; i += 1) {
sum(1, i);
}

performance.mark('first_end');


performance.mark('second_start');

for (let i = 0; i < loopCount; i += 1) {
sum(1, i);
}

performance.mark('second_end');

performance.measure('first_measure', 'first_start', 'first_end');
const first_measures = performance.getEntriesByName('first_measure');
console.log(first_measures[0]);

performance.measure('second_measure', 'second_start', 'second_end');
const second_measures = performance.getEntriesByName('second_measure');
console.log(second_measures[0]);

上方利用 Node.js v18.1 的 perf_hooks 做执行速度的测量,执行结果如下:

图片

执行后会发现第一次执行的时间花了 8 秒,第二次的执行时间只花了 6 秒,大家可以再把 loopCount 的数字改大一点,差距会越来越明显。

但是这麽做仍然没办法确认是 TurboFan 动了手脚,因此接下来执行的时候,加上 --trace-opt 的 flag,看看 Optimization 是否有发生:

图片

执行后的信息显示了 TurboFan 做的几次 Optimization,也有把每次 Optimization 的原因写下来,像第一二行分别显示了原因为 hot and stable 和 small function,这些都是 TurboFan 背后做的 Optimization 策略。

那 Deoptimization 的部分呢?要测试也很简单,只要把第二个迴圈的参数型别改成 String 送给 sum 函式执行,那 TurboFan 就会进行 Deoptimization,为了查看 Deoptimization 的讯息,下方执行的时候再加上 --trace-deopt:

图片

在 highlight 的那一段,就是因为送入 sum 的参数型别不同,所以执行了 Deoptimization,但是接下来又因为一直送 String 进 sum 执行,所以 TurboFan 又会再替 sum 重新做 Optimization。

总结

整理 V8 引擎执行 JavaScript 的过程后,能够得出下方的流程图:

图片

搭配上图解说 V8 引擎如何执行 JavaScript:

  • Parser 透过 Scanner 的 Tokens 将 JavaScript 解析成 AST
  • Ignition 把 AST 解释成 ByteCode 执行,并且在执行时搜集类型信息
  • TurboFan 针对信息将 ByteCode 再编译为机器码
  • 如果机器码检查到这次的执行和之前 Optimization 策略不同,就做 Deop timization 回到 ByteCode,以继续搜集类型信息或放弃治疗。

作者:神Q超人 > 来源:medium

原文:https://medium.com/tarbugs/%E5%9F%B7%E8%A1%8C-javascript-%E7%9A%84-v8-%E5%BC%95%E6%93%8E%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC-f97e5b4b3fbe

作者:Andy Chen  

译者:前端小智

来源:medium 

原文:https://medium.com/starbugs/%E5%8E%9F%E4%BE%86%E7%A8%8B%E5%BC%8F%E7%A2%BC%E6%89%93%E5%8C%85%E4%B9%9F%E6%9C%89%E9%80%99%E9%BA%BC%E5%A4%9A%E7%9C%89%E8%A7%92-%E6%B7%BA%E8%AB%87-tree-shaking-%E6%A9%9F%E5%88%B6-8375d35d87b2​

责任编辑:武晓燕 来源: 大迁世界
相关推荐

2022-09-16 08:32:25

JavaC++语言

2023-06-05 16:38:51

JavaScript编程语言V8

2020-10-12 06:35:34

V8JavaScript

2023-10-10 10:23:50

JavaScriptV8

2009-07-20 09:36:04

谷歌浏览器安全漏洞

2022-10-24 09:11:05

TypeScriptV8

2020-10-12 14:59:31

V8引擎如何执行Jav

2017-12-17 16:34:18

JavaScript代码V8

2020-09-27 07:32:18

V8

2022-06-21 08:52:47

Node.js服务端JavaScript

2022-02-25 08:32:07

nodemon搭Node.jsJavascript

2020-10-25 08:22:28

V8 引擎JavaScript回调函数

2019-05-28 10:24:31

V8JavaScript延迟

2022-11-04 07:12:24

JavaScript基准测试

2009-08-21 10:09:02

Google ChroV8引擎linux系统

2010-07-20 16:35:52

V8JavaScript浏览器

2019-11-28 10:53:19

程序员技能开发者

2023-06-07 16:00:40

JavaScriptV8语言

2016-04-18 09:33:52

nodejswebapp

2023-12-27 18:16:39

MVCC隔离级别幻读
点赞
收藏

51CTO技术栈公众号