Skip to content

V8 引擎原理

V8 引擎简介

V8 是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,使用 C++ 编写。它被用于:

  • Chrome 浏览器
  • Node.js
  • Electron
  • Deno

核心特点

  • 即时编译(JIT)而非解释执行
  • 高效的垃圾回收机制
  • 隐藏类和内联缓存优化
  • 多线程编译和优化

V8 引擎架构

JavaScript 代码

【解析器 Parser】

抽象语法树 (AST)

【Ignition 解释器】

字节码 (Bytecode)

【TurboFan 优化编译器】

优化的机器码

主要组件

  1. Parser(解析器):将 JavaScript 源码转换为 AST
  2. Ignition(解释器):将 AST 转换为字节码并执行
  3. TurboFan(优化编译器):将热点代码编译为优化的机器码
  4. Orinoco(垃圾回收器):管理内存分配和回收

JavaScript 代码执行流程

1. 解析阶段(Parsing)

词法分析(Lexical Analysis)

将源代码分解为 tokens(词法单元)。

javascript
// 源代码
const sum = a + b;

// 词法分析后的 tokens
[
  { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'sum' },
  { type: 'Punctuator', value: '=' },
  { type: 'Identifier', value: 'a' },
  { type: 'Punctuator', value: '+' },
  { type: 'Identifier', value: 'b' },
  { type: 'Punctuator', value: ';' }
]

语法分析(Syntax Analysis)

将 tokens 转换为抽象语法树(AST)。

javascript
// 源代码
function add(a, b) {
  return a + b;
}

// 简化的 AST
{
  type: 'FunctionDeclaration',
  id: { type: 'Identifier', name: 'add' },
  params: [
    { type: 'Identifier', name: 'a' },
    { type: 'Identifier', name: 'b' }
  ],
  body: {
    type: 'BlockStatement',
    body: [{
      type: 'ReturnStatement',
      argument: {
        type: 'BinaryExpression',
        operator: '+',
        left: { type: 'Identifier', name: 'a' },
        right: { type: 'Identifier', name: 'b' }
      }
    }]
  }
}

预解析(Pre-parsing)

  • V8 采用延迟解析策略
  • 首次加载时只完全解析立即执行的代码
  • 函数体在首次调用时才完全解析
javascript
function outer() {
  // outer 被立即解析
  
  function inner() {
    // inner 首次只做预解析(检查语法错误)
    // 在 inner 被调用时才完全解析
    console.log('inner');
  }
  
  return inner;
}

2. Ignition 解释器

Ignition 将 AST 转换为字节码并执行。

为什么使用解释器?

  • 快速启动(无需等待编译)
  • 节省内存(字节码比机器码小)
  • 收集性能分析数据(为优化编译做准备)

字节码示例

javascript
function add(a, b) {
  return a + b;
}

// 对应的字节码(简化)
[
  Ldar a1      // 加载参数 a
  Add a2       // 加上参数 b
  Return       // 返回结果
]

寄存器机制

V8 的字节码基于寄存器架构(而非栈架构):

javascript
function calculate(x) {
  const a = x + 1;
  const b = a * 2;
  return b;
}

// 字节码(概念示例)
[
  Ldar r0          // 加载 x 到累加器
  AddSmi [1]       // 加 1(Small Integer)
  Star r1          // 存储到寄存器 r1 (a)
  Ldar r1          // 加载 a
  MulSmi [2]       // 乘 2
  Star r2          // 存储到寄存器 r2 (b)
  Ldar r2          // 加载 b
  Return           // 返回
]

3. TurboFan 优化编译器

当某段代码被频繁执行(热点代码),TurboFan 会将其编译为高度优化的机器码。

优化触发条件

javascript
function hotFunction(x) {
  return x * 2;
}

// 调用次数少:由 Ignition 执行字节码
for (let i = 0; i < 100; i++) {
  hotFunction(i);
}

// 调用次数多:TurboFan 优化编译为机器码
for (let i = 0; i < 10000; i++) {
  hotFunction(i);  // 在某个阈值后被优化
}

推测性优化(Speculative Optimization)

TurboFan 基于运行时收集的类型信息进行优化。

javascript
function add(a, b) {
  return a + b;
}

// 场景 1:始终传入数字
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);  // V8 假设 a 和 b 总是数字,生成优化代码
}

// 场景 2:突然传入字符串
add('hello', 'world');  // 类型假设被打破,触发去优化(Deoptimization)

去优化(Deoptimization)

当优化假设被打破时,V8 会退回到字节码执行:

javascript
function calculate(x) {
  return x * 2;
}

// 第一阶段:传入数字,被优化为数字运算
for (let i = 0; i < 10000; i++) {
  calculate(i);  // 优化:假设 x 是数字
}

// 第二阶段:传入字符串,触发去优化
calculate('5');  // 去优化:回到字节码执行

// 第三阶段:继续传入不同类型
calculate(100);   // 可能不会再次优化
calculate('10');  // 类型不稳定

避免去优化的建议

  • 保持函数参数类型一致
  • 避免在对象中添加/删除属性
  • 避免改变对象的原型

内联缓存(Inline Caching,IC)

内联缓存是 V8 提升属性访问性能的关键技术。

工作原理

javascript
function getName(obj) {
  return obj.name;
}

const person1 = { name: 'Alice', age: 25 };
const person2 = { name: 'Bob', age: 30 };

// 第一次调用
getName(person1);
// V8 记录:obj 的形状(Shape/Hidden Class)+ name 的位置

// 第二次调用相同形状的对象
getName(person2);
// V8 快速查找:检查形状匹配,直接访问缓存的位置

// 调用不同形状的对象
getName({ name: 'Charlie' });
// 形状不匹配,需要重新查找

IC 状态

  1. 未初始化(Uninitialized):从未执行过
  2. 单态(Monomorphic):只见过一种类型/形状
  3. 多态(Polymorphic):见过 2-4 种类型/形状
  4. 超态(Megamorphic):见过超过 4 种类型/形状
javascript
function getProperty(obj) {
  return obj.x;
}

// 单态:性能最优
const obj1 = { x: 1, y: 2 };
const obj2 = { x: 3, y: 4 };
getProperty(obj1);
getProperty(obj2);  // 相同形状,单态 IC

// 多态:性能较好
const obj3 = { x: 5, z: 6 };     // 不同形状 1
const obj4 = { x: 7 };           // 不同形状 2
const obj5 = { y: 8, x: 9 };     // 不同形状 3
getProperty(obj3);
getProperty(obj4);
getProperty(obj5);  // 多态 IC

// 超态:性能差
for (let i = 0; i < 10; i++) {
  const obj = {};
  obj['prop' + i] = i;  // 每个对象形状都不同
  obj.x = i;
  getProperty(obj);  // 最终变成超态 IC
}

隐藏类(Hidden Classes / Shapes / Maps)

V8 使用隐藏类来优化对象属性访问。

隐藏类的创建

javascript
// 示例 1:相同的属性顺序,共享隐藏类
function Point(x, y) {
  this.x = x;  // 隐藏类 C0 -> C1
  this.y = y;  // 隐藏类 C1 -> C2
}

const p1 = new Point(1, 2);  // 使用隐藏类序列 C0 -> C1 -> C2
const p2 = new Point(3, 4);  // 复用相同的隐藏类序列
javascript
// 示例 2:不同的属性顺序,不同的隐藏类
const obj1 = {};
obj1.x = 1;  // 隐藏类 M0 -> M1
obj1.y = 2;  // 隐藏类 M1 -> M2

const obj2 = {};
obj2.y = 2;  // 隐藏类 M0 -> M3(不同的路径)
obj2.x = 1;  // 隐藏类 M3 -> M4

// obj1 和 obj2 最终的隐藏类不同!

优化建议

1. 保持对象形状一致

javascript
// ❌ 不良实践:不同的属性顺序
function createPerson(name, age, hasAge) {
  const person = { name };
  if (hasAge) {
    person.age = age;  // 有时有 age,有时没有
  }
  return person;
}

// ✅ 良好实践:相同的属性顺序
function createPerson(name, age) {
  return {
    name,
    age: age ?? null  // 始终包含 age 属性
  };
}

2. 在构造函数中初始化所有属性

javascript
// ❌ 不良实践
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  addZ(z) {
    this.z = z;  // 动态添加属性,改变隐藏类
  }
}

// ✅ 良好实践
class Point {
  constructor(x, y, z = 0) {
    this.x = x;
    this.y = y;
    this.z = z;  // 在构造时就初始化所有属性
  }
}

3. 避免删除属性

javascript
// ❌ 不良实践
const obj = { x: 1, y: 2, z: 3 };
delete obj.z;  // 删除属性会触发隐藏类转换,性能差

// ✅ 良好实践
const obj = { x: 1, y: 2, z: 3 };
obj.z = undefined;  // 保持形状不变
// 或者使用 null
obj.z = null;

4. 使用数组存储动态数据

javascript
// ❌ 不良实践:动态属性
const obj = {};
for (let i = 0; i < 100; i++) {
  obj['key' + i] = i;  // 创建 100 个不同的隐藏类
}

// ✅ 良好实践:使用 Map 或数组
const map = new Map();
for (let i = 0; i < 100; i++) {
  map.set('key' + i, i);
}

V8 的数组优化

V8 为不同类型的数组使用不同的内部表示。

数组元素类型(Elements Kind)

javascript
// PACKED_SMI_ELEMENTS:小整数数组
const arr1 = [1, 2, 3, 4, 5];

// PACKED_DOUBLE_ELEMENTS:浮点数数组
const arr2 = [1.1, 2.2, 3.3];

// PACKED_ELEMENTS:混合类型数组
const arr3 = [1, 'hello', {}, null];

// HOLEY_SMI_ELEMENTS:带空洞的整数数组
const arr4 = [1, 2, , 4];  // 注意索引 2 是空的

// DICTIONARY_ELEMENTS:稀疏数组
const arr5 = [];
arr5[1000] = 'sparse';

数组类型转换(不可逆)

javascript
// 转换链(单向,不可逆)
PACKED_SMI_ELEMENTS

PACKED_DOUBLE_ELEMENTS

PACKED_ELEMENTS

HOLEY_ELEMENTS

// 示例
const arr = [1, 2, 3];  // PACKED_SMI_ELEMENTS

arr.push(1.5);          // 转换为 PACKED_DOUBLE_ELEMENTS

arr.push('text');       // 转换为 PACKED_ELEMENTS

arr[10] = 'sparse';     // 转换为 HOLEY_ELEMENTS(永久性能下降)

数组性能优化建议

1. 避免创建空洞

javascript
// ❌ 不良实践
const arr = [1, 2, 3];
arr[10] = 10;  // 创建空洞 [1, 2, 3, empty × 7, 10]

// ✅ 良好实践
const arr = [1, 2, 3];
for (let i = 4; i <= 10; i++) {
  arr.push(i);
}

2. 保持数组元素类型一致

javascript
// ❌ 不良实践
const arr = [1, 2, 3];
arr.push('4');  // 类型不一致,性能下降

// ✅ 良好实践
const arr = [1, 2, 3];
arr.push(4);

3. 预分配数组大小

javascript
// ❌ 不良实践
const arr = [];
for (let i = 0; i < 10000; i++) {
  arr.push(i);  // 多次重新分配内存
}

// ✅ 良好实践
const arr = new Array(10000);
for (let i = 0; i < 10000; i++) {
  arr[i] = i;
}

4. 使用数组方法时注意

javascript
// delete 会创建空洞
const arr = [1, 2, 3, 4, 5];
delete arr[2];  // ❌ [1, 2, empty, 4, 5]

// 使用 splice 代替
arr.splice(2, 1);  // ✅ [1, 2, 4, 5]

编译管道(Compilation Pipeline)

字节码 → 机器码的优化层级

【未优化】

Ignition 字节码

【热点检测】

TurboFan 优化

【高度优化的机器码】

优化的条件

  1. 函数被频繁调用
  2. 类型反馈信息充足
  3. 没有去优化历史
javascript
// 例子:优化友好的代码
function add(a, b) {
  return a + b;
}

// 场景 1:类型稳定,容易优化
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);  // 始终是数字
}

// 场景 2:类型不稳定,难以优化
for (let i = 0; i < 10000; i++) {
  if (i % 2 === 0) {
    add(i, i + 1);        // 数字
  } else {
    add(String(i), '!');  // 字符串
  }
}

调试和性能分析

Node.js 命令行选项

bash
# 打印优化/去优化信息
node --trace-opt --trace-deopt script.js

# 打印内联缓存状态
node --trace-ic script.js

# 允许手动 GC
node --expose-gc script.js

# 打印字节码
node --print-bytecode script.js

# 打印优化的代码
node --print-opt-code script.js

查看 V8 内部信息

javascript
// 使用 %函数名% 访问 V8 内部函数(需要 --allow-natives-syntax)

// 检查优化状态
function testFunction(x) {
  return x * 2;
}

// 触发优化
for (let i = 0; i < 10000; i++) {
  testFunction(i);
}

// 检查是否被优化(需要 --allow-natives-syntax)
// %OptimizeFunctionOnNextCall(testFunction);
// testFunction(10);
// console.log(%GetOptimizationStatus(testFunction));

Chrome DevTools

  1. Performance 面板

    • 查看脚本解析时间
    • 查看函数执行时间
    • 查看优化/去优化事件
  2. Memory 面板

    • 堆快照
    • 内存分配时间线
  3. Coverage 面板

    • 查看未使用的代码

性能优化最佳实践

1. 编写优化友好的代码

javascript
// ✅ 类型稳定
function calculate(x) {
  return x * 2 + 1;
}

// ✅ 单态函数
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  distance() {
    return Math.sqrt(this.x ** 2 + this.y ** 2);
  }
}

// ✅ 避免 try-catch 在热路径中
function hotFunction(x) {
  // 不要在这里使用 try-catch
  return x * 2;
}

function safeCaller(x) {
  try {
    return hotFunction(x);
  } catch (e) {
    return 0;
  }
}

2. 避免性能陷阱

javascript
// ❌ arguments 对象(在严格模式下影响较小)
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

// ✅ 使用剩余参数
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

// ❌ with 语句(禁用优化)
with (obj) {
  // ...
}

// ❌ eval(禁用优化)
eval('x = 10');

3. 合理使用数据结构

javascript
// 小数据集:对象或数组
const small = { a: 1, b: 2, c: 3 };

// 大数据集或动态键:Map
const large = new Map();
for (let i = 0; i < 1000; i++) {
  large.set(`key${i}`, i);
}

// 唯一值:Set
const unique = new Set([1, 2, 2, 3, 3, 3]);

4. 函数优化

javascript
// ✅ 小函数更容易被内联
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// ✅ 单一职责
function calculate(x, y) {
  return multiply(add(x, y), 2);
}

// ❌ 过于复杂的函数难以优化
function complexFunction(x, y, z, options) {
  // 100+ 行代码
  // 多个分支
  // 多种类型处理
}

总结

V8 的关键特性

  1. JIT 编译:结合解释执行和编译执行的优点
  2. 隐藏类:优化对象属性访问
  3. 内联缓存:加速属性和方法查找
  4. 推测性优化:基于类型反馈生成高效代码
  5. 高效 GC:分代回收和增量标记

编写高性能代码的原则

  1. 类型稳定性:保持变量类型一致
  2. 形状一致性:保持对象结构稳定
  3. 避免去优化:不要打破优化假设
  4. 合理使用数据结构:选择适合场景的容器
  5. 注意性能陷阱:避免使用影响优化的特性

参考资源

MIT Licensed | Keep Learning.