关于前端:面试写说说执行-JavaScript-的-V8-引擎做了什么

38次阅读

共计 4332 个字符,预计需要花费 11 分钟才能阅读完成。

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

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 能够从上到下,由左而右去了解它在执行的步骤:

  1. 走 VariableDeclaration 建设名字为 name 的变量
  2. ExpressionStatement 到表达式
  3. AssignmentExpression 遇到 =,且右边为 name,左边为字串 Clark

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

JIT(Just-In-Time)

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

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

编译语言的益处是能够在执行前的编译阶段,扫视所有的代码,将能够做的优化都实现,但直译语言就无奈做到这一点,因为执行时才开始解释的关係,执行上就绝对较慢,也没方法在一开始做优化,为了解决这个情况,JIT 呈现了。

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

听起来 JIT 超讚,而在 V8 引擎裡负责解决 JIT 的左右手别离为 IgnitionTurboFan

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:

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

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

原文:https://medium.com/tarbugs/%E…

交换

文章每周继续更新,能够微信搜寻「大迁世界」第一工夫浏览和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 曾经收录,整顿了很多我的文档,欢送 Star 和欠缺,大家面试能够参照考点温习,另外关注公众号,后盾回复 福利,即可看到福利,你懂的。

正文完
 0