编程语言研究书库

10 隐藏类与内联缓存:V8 的对象性能学

10 隐藏类与内联缓存:V8 的对象性能学

这一章讲什么

07 章说过:JavaScript 的对象是动态映射,找属性要「搜索」,而 C++ 只要 「跳到已知偏移」。如果每次访问属性都要真搜一遍,JavaScript 的对象操作 会慢到不可用。这一章讲 V8 的破局法,两件配套的机关:

  1. 隐藏类(hidden class,也叫 shape 或 map):给对象结构建档, 结构相同的对象共享同一张档案;
  2. 内联缓存(inline cache, IC):记住「上次在哪找到的」, 下次形状没变就直接跳。

以及代价侧:你的哪些写法会让档案不停重建、让缓存不停作废, 最后把对象整个踢进「字典模式」。这一章直接回应 07 章埋的话: 「不理解这个搜索过程和它的内部结构,就是性能问题的主要来源。」

顶层全景:从「搜索」到「跳偏移」

  慢速 naive 版:每次读 obj.prop 都在属性集合里线性搜 → 随属性数变慢
     │
     │ V8 的破局
     ▼
  隐藏类:这个对象有 propA、propB,各占第几个槽位 → 一张「形状档案」
     │   形状相同的 1000 个对象,共享同一张档案
     ▼
  内联缓存:这个函数上次在「形状 S」上找 prop,是在第 2 个槽 → 记住,直接跳
     │   这次对象的形状还是 S?是 → 跳;不是 → 重新找,记新答案
     ▼
  失效太多次 → V8 放弃 → 对象进字典模式(哈希表,灵活但慢)

  图说:快与慢的分水岭不是「对象大不大」,而是「形状稳不稳」。

核心原理

3.1 隐藏类:对象的形状档案

你创建一个对象,V8 就给它建一张隐藏类——一份结构蓝图,写清 这个对象有哪些属性、什么类型、各存在内存的哪个位置1。 可以把它想成 07 章那些描述符的「汇总页」:描述符管单个属性的行为, 隐藏类管整个对象的布局。

关键机制是形状会随属性的增删改而变:对象初生时有一个初始形状, 之后每加一个属性、改一次定义、删一个属性,形状就变一次—— 书里的原话:「创建一个空对象再逐个加十个属性,这个对象就先后有过 十一种形状」2。而且这些形状像 08 章的原型链一样层层叠加, 每次访问属性都要沿形状链查到属性所属的那一层3。 所以结论直接:创建对象时就把属性定全,让形状一次到位4。

主线例子:两种建对象的方式,两种命运。

好版本——属性一次建全、类型一致、顺序固定:

function createOptimizedDrawer(label, contents) {
  return { label, contents, isOpen: false, accessCount: 0 };
  //      ↑ 字符串   ↑ 字符串    ↑ 布尔      ↑ 数字 —— 每次都一样
}
const drawer1 = createOptimizedDrawer('Tax', 'tax documents');
const drawer2 = createOptimizedDrawer('Legal', 'contracts');
const drawer3 = createOptimizedDrawer('HR', 'personnel files');

三个对象共享同一张隐藏类——属性已知、值类型一致、顺序一致, V8 建一份档案反复用5。

坏版本——创建后再按条件逐个加,而且加的顺序还随机:

function createProblematicDrawer(label, contents, config = {}) {
  const drawer = { label, contents };      // 形状 1
  if (config.secure) {
    drawer.lockCode = config.lockCode;     // 形状 2(不一定发生)
  }
  if (config.automated) {
    drawer.motorSpeed = config.motorSpeed; // 形状 3、4(不一定发生)
    drawer.sensorType = config.sensorType;
  }
  if (Math.random() > 0.5) {
    drawer.priority = 'high';              // 形状 5、6……顺序还分两种
    drawer.department = config.department;
  } else {
    drawer.department = config.department; // 另一种顺序 = 另一种形状!
    drawer.priority = 'high';
  }
  return drawer;    // 返回之前,形状最多已经漂移了五次[^6]
}

条件加成让每个对象的长相不确定,顺序随机让「同样的属性集」也分裂成 不同形状。V8 无法预知、无法共享档案,只能见一个建一个。 书里那句经验法则就贴在例子后面:形状不一致,代码就慢6。

3.2 构造函数的隐藏特权

「那我在构造函数里逐个 this.x = ... 不也是逐个加属性?」 ——技术上是,但 8 章的 class 机制在这里兑现了它的性能价值: 构造函数(以及 class)里逐个赋值也会产生多个过渡形状, 但引擎高度优化这条路径:第一次创建时 V8 就学到了最终形状, 后续每次调用直接一步到位7。而同样的逐个赋值写在「裸的」 const drawer = {}; drawer.x = ... 里,V8 没有函数可以关联这个模式, 换个运行场合就得重新摸索一遍形状8。

所以书里的明确偏好:构造函数和 class,永远优于 obj.prop = value 式的散装构建——不是因为它们省形状转换,而是因为 V8 能学到模式、 优化未来的实例化9。这也是 07 章 defineProperties「一次定义多个更高效」 的真正答案。

3.3 内联缓存(IC):记住上次的位置

隐藏类解决「档案从哪来」;内联缓存解决「查档要不要每次重来」。 机制:某处代码执行 obj.prop,V8 找到属性在某个隐藏类的第 N 槽, 就把「形状 → 第 N 槽」记在这段代码旁边(所以叫内联)。 下次执行,先比形状:相同 → 直接按槽位取,整个查找跳过; 不同 → 作废,重新查,记新答案10。

失效是有代价的,而代价会升级:形状老变,IC 里的记录老作废; 作废太多次,V8 的判断很人类——放弃,把这段代码访问的对象 踢进慢速通道,从此什么优化都不再给11。这个过程叫去优化 (deoptimization),它对开发者完全隐形,唯一的症状是「代码变 慢了」12。

隐形到什么程度?书里自己承认:隐藏类问题极难诊断, 好在 Chrome devtools 这类工具能暴露应用生成了多少隐藏类13—— 「我们后面的章节会讲这些工具」,而后面的章节,MEAP 还没有。

3.4 快速属性 vs 字典模式:最后一次降级

V8 的属性存法分两档14:

模式存法访问
快速属性线性内存布局,隐藏类直接给偏移常数时间,按偏移取
字典(慢)属性哈希(按内容算出位置的)表灵活(随便增删改),但每次要重新算位置

对象什么时候被降级?「太不可预测」的时候:动态加的属性太多、 属性被删、形状五花八门——V8 换哈希表,用性能换灵活15。 第 5 章的总结把收益说得很直白:形状一致时,内联缓存加持的属性访问 比字典模式快 100 倍16。

3.5 稳定形状清单(书里的五条)

要保持快道,原书给了五条可执行的建议17:

  1. 属性在构造函数里一次初始化全,别事后补;
  2. 形状保持一致:同样的属性、同样的顺序——顺序不同就是不同形状, { label, isLocked } 和 { isLocked, label } 在 V8 眼里是两种对象18;
  3. 别 delete 属性——可能逼出字典模式;
  4. 数组用数字下标,别和命名属性混用(09 章的存储机制在这里收尾);
  5. 属性的值别换类型(字符串改数字这类)。

3.6 这不是教科书练习:Node.js 自身的整改

作者把这套优化用到了自己工作的地方,作为本章的收束,也是全书 「实现知识→生产收益」路径的最佳案例:十年前他刚开始给 Node.js 贡献代码时,Node 内部也是一处一处给对象加属性;后来全面改成 「属性齐了才建对象、顺序固定、类型一致」,全世界所有跑在 Node.js 上的应用都因此变快了19。引擎优化不是象牙塔知识, 是运营一个运行时平台时的日常决策。

作者的判断与证据

有证据的判断:

  • 「十一种形状」「最多五次形状漂移」是机制推演,基于隐藏类的公开实现原理, 书中自己注明「有意做了简化」并指向 V8 官方文档20。
  • 「快 100 倍」出自第 5 章总结,方向与 V8 官方对 IC 的叙述一致21。

作者的主张:

  • 「构造函数/ class 永远优于散装构建」是性能导向的实践立场; 反例(一次性脚本、形状本来就多样的场景)书中未讨论。
  • 五条清单是经验法则,不是规范;不同引擎的阈值和行为不同。

判断(我们的,不是书里的): 10 章是全书的「点题章」:ConsString(03)、 SMI(05)、隐藏类(10)是同一个思想的三次应用——引擎给「最常见、最有规律」的 用法修专用快道,规律破坏时静默降级。记住这个结构,比记住五诫中的任何一条 都更抗版本过时。 如果错,会错在: 如果未来的运行时对「无规律代码」也能保持接近优化的速度 (比如靠更强的推断或全新的对象布局),「守形状」会从性能必需降级为风格偏好—— 但「降级无提示、只能靠工具观察」的现象本身不会消失,只是代价变小。

边界与局限

  • 全书对 V8 优化的讲解自我声明为简化版,作者建议想深入的人去读 V8 官方文档(v8.dev/docs/hidden-classes)22。真实实现还有 属性线程内缓存、反馈向量(V8 记录「这段代码见过什么形状」的内部表)等本书未涉及的层。
  • 所有机制都是实现层,不是规范:其他引擎有类似机制但细节不同, 且 V8 版本间会变(03 章那个「优化会过时」的警告在此同样适用)。
  • Object.setPrototypeOf 会迫使形状重建(08 章留的尾巴): 动态换原型的性能代价,原书没有明说,但机制上可以推出—— 形状包含原型,换原型 = 换形状。

可带走的

  1. 隐藏类 = 对象的结构档案;属性一次建全,档案一次到位。
  2. 空对象逐个加十个属性 = 十一种形状;条件加属性 + 随机顺序 = 形状灾难。
  3. 构造函数/class 的价值之一:V8 能学到最终形状,后续实例化一步到位。
  4. 内联缓存记「形状→槽位」;形状变,缓存废;废太多,去优化。
  5. 属性顺序不同 = 形状不同;delete 可能触发字典模式。
  6. 快/字典两档存法;优化到位时属性访问比字典快 100 倍。
  7. 诊断形状问题:Chrome devtools;症状只有一个——慢。
  8. Node.js 自己这么改过:生产系统的全局提速就是这么来的。

原文地图

主题原书章原文位置
隐藏类的定义5.8 Hidden classes and shapes: the secret object blueprinttext/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:7(搜「shapes or maps」)
形状随增删改而变同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:19(搜「shape changes」)
「十一种形状」与一次建全同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:25(搜「11 different shapes」) · :38(搜「all at once」)
构造函数的优化特权同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:45(搜「What about constructors」) · :94(搜「learn the pattern」)
坏例子:五次形状漂移同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:138(搜「bad example」) · :186(搜「five times」)
「形状不一致 = 慢」同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:144(搜「inconsistent shapes」)
隐形难诊断同上text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:199(搜「difficult to diagnose」)
内联缓存与放弃治疗5.9 Inline caches: V8's speed boost systemtext/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:7(搜「give up」) · :7(搜「deoptimization」)
快速/字典两档同上text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:40(搜「Fast properties」) · :48(搜「hash table」)
五条实践建议同上text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:66(搜「Initialize all properties」)
顺序不同 = 形状不同同上text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:83(搜「different orders」)
Node.js 自身整改同上text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:115(搜「A decade ago」)
「快 100 倍」总结5.10 Summarytext/25-ch05-10-5-10-summary.txt:40(搜「100x faster」)

Footnotes

  1. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 7 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:7,搜「structural blueprints」)。「隐藏类里存着属性描述符」见第 13 段(搜「where those are held」)。 ↩

  2. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 19 段与第 25 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:25,搜「11 different shapes」)。 ↩

  3. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 25 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:25,搜「very similar to the way the prototype chain works」)。 ↩

  4. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 38 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:38,搜「defined all at once」)。 ↩

  5. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 102–128 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:102,搜「same shape」)。「共享同一份内部表示」见第 118 段(搜「reuses it」)。 ↩

  6. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 144 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:144,搜「inconsistent shapes means slower」)。 ↩

  7. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 51–67 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:67,搜「each individual assignment」;:67 搜「knows the final shape」)。 ↩

  8. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 73–88 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:88,搜「no constructor to associate」)。 ↩

  9. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 94 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:94,搜「always preferable」)。 ↩

  10. 出处:「5.9 Inline caches: V8's speed boost system」第 7 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:7,搜「remember where properties are located」)。 ↩

  11. 出处:「5.9 Inline caches: V8's speed boost system」第 7 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:7,搜「give up and force your objects into a slow mode」)。 ↩

  12. 出处:「5.9 Inline caches: V8's speed boost system」第 7 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:7,搜「completely invisible」)。 ↩

  13. 出处:「5.8 Hidden classes and shapes: the secret object blueprint」第 199 段(text/23-ch05-08-5-8-hidden-classes-and-shapes-the-secret-object-.txt:199,搜「difficult to diagnose」)。 ↩

  14. 出处:「5.9 Inline caches: V8's speed boost system」第 34–43 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:40,搜「Fast properties」;:43 搜「Slow properties」)。 ↩

  15. 出处:「5.9 Inline caches: V8's speed boost system」第 48 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:48,搜「too unpredictable」)。 ↩

  16. 出处:「5.10 Summary」第 40 段(text/25-ch05-10-5-10-summary.txt:40,搜「100x faster」)。 ↩

  17. 出处:「5.9 Inline caches: V8's speed boost system」第 60–78 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:66,搜「Initialize all properties in constructors」)。 ↩

  18. 出处:「5.9 Inline caches: V8's speed boost system」第 83–103 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:83,搜「different orders」;:90 搜「drawer1 = {」)。 ↩

  19. 出处:「5.9 Inline caches: V8's speed boost system」第 115 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:115,搜「A decade ago」与「greatly boost the performance」)。 ↩

  20. 出处:「5.9 Inline caches: V8's speed boost system」第 20 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:20,搜「intentionally oversimplifying」)与 v8.dev 文档的指引(同段)。 ↩

  21. 出处:「5.10 Summary」第 40 段(text/25-ch05-10-5-10-summary.txt:40,搜「100x faster」)。补充(不在书里):V8 官方博客对内联缓存的解释见 v8.dev/docs/hidden-classes,与本书机制叙述一致。 ↩

  22. 出处:「5.9 Inline caches: V8's speed boost system」第 20 段(text/24-ch05-09-5-9-inline-caches-v8-s-speed-boost-system.txt:20,搜「v8.dev/docs/hidden-classes」)。 ↩

On this page