type
Post
status
Published
date
May 29, 2023
slug
javascript-v8-turbo
summary
V8引擎的即时编译(JIT)策略显著提高了从JavaScript生成可执行代码的速率(编译器TurboFan的功劳),我们有必要对这个机制有基本的认识,从而避免写出一些影响性能的代码。
tags
思考
开发
前端
category
学习思考
icon
password
Property
Oct 8, 2023 09:02 AM
前端性能优化中常被我们忽略的一点是在引擎层面JavaScript被解析和执行耗费的时间。对于V8引擎来说,JavaScript被解析器解析为抽象语法树(AST)后,会进入一个解释器+编译器的组合管道执行,解释器会输出字节码,而编译器直接输出机器代码(https://v8.dev/blog/launching-ignition-and-turbofan)。
形象的V8引擎点火器 + 涡轮风扇的组合
形象的V8引擎点火器 + 涡轮风扇的组合
V8引擎的这种即时编译(JIT)策略显著提高了从JavaScript生成可执行代码的速率(编译器TurboFan的功劳),我们有必要对这个机制有基本的认识,从而避免写出一些影响性能的代码。

Inigition和TurboFan

notion image
上图阐述了一个JavaScript文件如何在浏览器中被执行的过程,首先从远程服务器上获取这个文件(由于网络的延迟不是小问题,我们的优化经常在这一步,例如减少JavaScript bundle的大小以提升首屏加载的速度),获取到完整的源文件后,浏览器会解析这个文件,在内存中生成AST数据结构,交给Ignition加上TurboFan的管道执行。
Ignition(点火器)顾名思义是先开始工作的,它生成字节码(并非CPU直接执行的机器码),执行速率相对没有那么快。这时候TurboFan编译器开始工作了,它会根据程序执行的历史来进行一些假定,试图对高频率执行的部分代码直接编译为机器码以供快速执行。

小实验

借助user timing API(https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_API/User_timing),我们可以测量程序运行的时间。下面我们来看一个例子,重复执行某一段代码:
import { performance } from "perf_hooks"; let iterations = 1e8; const a = 1; const b = 2; const add = (x: any, y: any) => x + y; performance.mark("start"); while (iterations--) { add(a, b); } performance.mark("end"); performance.measure("My Special Benchmark", "start", "end"); const [measure] = performance.getEntriesByName("My Special Benchmark"); console.log(measure);
这是一段由TypeScript编写的Nodejs程序,我们将它编译为JavaScript:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const perf_hooks_1 = require("perf_hooks"); let iterations = 1e8; const a = 1; const b = 2; const add = (x, y) => x + y; perf_hooks_1.performance.mark("start"); while (iterations--) { add(a, b); } perf_hooks_1.performance.mark("end"); perf_hooks_1.performance.measure("My Special Benchmark", "start", "end"); const [measure] = perf_hooks_1.performance.getEntriesByName("My Special Benchmark"); console.log(measure);
使用Nodejs执行上面的代码:
[marking 0x09c7828d3389 <JSFunction (sfi = 0x323b1849b881)> for optimized recompilation, reason: hot and stable] [marking 0x09c7828d4699 <JSFunction add (sfi = 0x323b1849b939)> for optimized recompilation, reason: small function] [compiling method 0x09c7828d4699 <JSFunction add (sfi = 0x323b1849b939)> (target TURBOFAN) using TurboFan] [compiling method 0x09c7828d3389 <JSFunction (sfi = 0x323b1849b881)> (target TURBOFAN) using TurboFan OSR] [optimizing 0x09c7828d3389 <JSFunction (sfi = 0x323b1849b881)> (target TURBOFAN) - took 0.083, 0.417, 0.000 ms] [optimizing 0x09c7828d4699 <JSFunction add (sfi = 0x323b1849b939)> (target TURBOFAN) - took 0.125, 0.333, 0.041 ms] [completed optimizing 0x09c7828d4699 <JSFunction add (sfi = 0x323b1849b939)> (target TURBOFAN)] PerformanceMeasure { name: 'My Special Benchmark', entryType: 'measure', startTime: 27.794666051864624, duration: 49.99266695976257, detail: null }
node —trace-opt dist/hello.js
可以看到TurboFan进行了编译优化。
不妨试试修改代码:
import { performance } from "perf_hooks"; let iterations = 1e8; const a = 1; const b = 2; const add = (x: any, y: any) => x + y; performance.mark("start"); while (iterations--) { add(a, b); } iterations = 1e8; add("foo", "bar"); while (iterations--) { add(a, b); } performance.mark("end"); performance.measure("My Special Benchmark", "start", "end"); const [measure] = performance.getEntriesByName("My Special Benchmark"); console.log(measure);
将这段代码运行一下:
PerformanceMeasure { name: 'My Special Benchmark', entryType: 'measure', startTime: 26.50658416748047, duration: 265.9936249256134, detail: null }
注释掉add(”foo”, “bar”)行:
PerformanceMeasure { name: 'My Special Benchmark', entryType: 'measure', startTime: 27.4902081489563, duration: 101.20350003242493, detail: null }
可以看到时间显著减少了。
这里发生的事情事实上是因为中间对add的调用否定了TurboFan对这个函数签名的假定,它的类型假定失效,此时就会取消之前所做的编译优化,而等待接来下的循环执行了一段时间后才继续优化。
通过—trace-deopt参数可以查看这个过程:
[marking 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> for optimized recompilation, reason: hot and stable] [marking 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> for optimized recompilation, reason: small function] [compiling method 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN) using TurboFan] [compiling method 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> (target TURBOFAN) using TurboFan OSR] [optimizing 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> (target TURBOFAN) - took 0.083, 0.542, 0.000 ms] [optimizing 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN) - took 0.125, 0.292, 0.000 ms] [completed optimizing 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN)] [bailout (kind: deopt-soft, reason: Insufficient type feedback for call): begin. deoptimizing 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)>, opt id 1, bytecode offset 102, deopt exit 6, FP to SP delta 192, caller SP 0x00016aeea420, pc 0x00012884c8ac] [bailout (kind: deopt-eager, reason: not a Smi): begin. deoptimizing 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)>, opt id 0, bytecode offset 2, deopt exit 0, FP to SP delta 32, caller SP 0x00016aeea328, pc 0x00012884ca54] [marking 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> for optimized recompilation, reason: small function] [compiling method 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN) using TurboFan] [marking 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> for optimized recompilation, reason: hot and stable] [optimizing 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN) - took 0.083, 0.208, 0.042 ms] [completed optimizing 0x0abcdcc14701 <JSFunction add (sfi = 0x3092d43b1ae1)> (target TURBOFAN)] [compiling method 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> (target TURBOFAN) using TurboFan OSR] [optimizing 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)> (target TURBOFAN) - took 0.125, 0.333, 0.000 ms] [bailout (kind: deopt-soft, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x0abcdcc133f1 <JSFunction (sfi = 0x3092d43b1a29)>, opt id 3, bytecode offset 127, deopt exit 4, FP to SP delta 192, caller SP 0x00016aeea420, pc 0x00012884ce24] PerformanceMeasure { name: 'My Special Benchmark', entryType: 'measure', startTime: 28.826040983200073, duration: 268.1276671886444, detail: null }
如果我们在代码中加入V8指令,禁用TurboFan编译优化:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const perf_hooks_1 = require("perf_hooks"); let iterations = 1e8; const a = 1; const b = 2; const add = (x, y) => x + y; perf_hooks_1.performance.mark("start"); %NeverOptimizeFunction(add); while (iterations--) { add(a, b); } iterations = 1e8; // add("foo", "bar"); while (iterations--) { add(a, b); } perf_hooks_1.performance.mark("end"); perf_hooks_1.performance.measure("My Special Benchmark", "start", "end"); const [measure] = perf_hooks_1.performance.getEntriesByName("My Special Benchmark"); console.log(measure);
PerformanceMeasure { name: 'My Special Benchmark', entryType: 'measure', startTime: 25.05691695213318, duration: 885.5741250514984, detail: null }
由此可见,如果完全不适用TurboFan进行编译优化,单纯通过解释器执行字节码速度将慢得多。
到这里我们应该对TurboFan所做的编译优化有一个基本了解了。
 
CSS选择器优先级重新学习TypeScript(基础)