Inline Cache
Inline Cache:JavaScript 引擎里最容易被误解的“缓存”
很多人第一次听到 Inline Cache,会下意识把它和 HTTP cache、memory cache、disk cache、bfcache 这类浏览器缓存放在一起理解。但这条路一开始就走偏了。
Inline Cache 不是资源缓存,不负责缓存网页、图片或脚本文件。它是 JavaScript 引擎内部的一种运行时优化机制,目标也很单纯:让动态语言里频繁出现的属性访问、方法调用、元素读写,尽量走上特化后的快速路径。
如果要先给一句最重要的定义,可以这样理解:
Inline Cache 记住的不是属性值,而是“某个访问点通常会遇到什么结构的对象,以及该如何更快地访问它”。
这篇文章主要回答 5 个问题:
- 为什么
obj.x这种代码值得专门优化 - Inline Cache 到底缓存了什么
- 为什么它叫
inline monomorphic、polymorphic、megamorphic分别是什么意思- 为什么它会提速,也为什么会退化
为什么 obj.x 值得单独优化?
先看一段最普通的代码:
1
2
3
function getX(obj) {
return obj.x;
}
表面看,这只是一次属性读取。但对 JavaScript 引擎来说,这里并没有那么“静态”。
引擎在执行 obj.x 时,至少要面对这些问题:
obj是不是普通对象x是对象自身属性,还是来自原型链x是普通值,还是 getter- 这次传进来的对象,和上次是不是同一种内部结构
- 对象是否在运行过程中被增删过属性,或改过原型
静态语言里,字段偏移量往往能在编译期确定;动态语言里,很多信息只能到运行时才知道。于是一次看似简单的属性读取,实际上可能退化成一次更通用的查找流程。
但 JavaScript 的真实执行轨迹并没有“动态到完全随机”。很多热点代码虽然语义上是动态的,运行时却很稳定。
1
2
3
function sumPoint(p) {
return p.x + p.y;
}
如果 sumPoint 大多数时候接收到的都是同一种结构:
1
2
3
{ x: 1, y: 2 }
{ x: 3, y: 4 }
{ x: 10, y: 20 }
那引擎就没有必要每次都从最通用的慢路径开始。它完全可以利用历史信息,把这个访问点优化得更便宜。
这就是 Inline Cache 要做的事。
Inline Cache 到底是什么?
最关键的一点是:
Inline Cache 缓存的不是属性值,而是访问路径。
很多人会误以为它做的是这种事:
上一次 obj.x = 42
所以下一次直接返回 42
这当然不成立,因为对象里的值可以变,但对象的结构稳定性依然可能成立。
更准确地说,Inline Cache 记录的是:
- 某个具体访问点见过哪些对象结构
- 对于这些结构,应该怎样更快地拿到目标属性
例如:
1
2
3
function getX(obj) {
return obj.x;
}
这里的 obj.x 在引擎眼里不是一个抽象概念,而是一个具体的访问点。引擎会在这个位置保留反馈信息,类似于:
- 这个位置见过一种结构
S1 - 对于
S1,属性x位于固定槽位 - 下次如果再见到
S1,就直接读取,不再走完整通用查找
也就是说,Inline Cache 做的是三件事:
- 观察这个访问点常见的运行时模式
- 为常见模式生成快速路径
- 未命中时回退慢路径,并继续积累反馈
从方法论上看,它其实就是“观测驱动的特化”。
为什么叫 Inline Cache?
inline 很容易让人想到“函数内联”,但这里重点并不在那个方向。
它更接近下面这层意思:
- 缓存和某个具体代码位置强绑定
- 快速路径就在这个操作点附近生效
- 它不是一个全局统一管理的缓存中心
所以它的重点不在“缓存”,而在“按访问点特化”。
下面三句代码虽然都在访问 .x:
1
2
3
a.x;
b.x;
c.x;
但在引擎看来,它们通常是三个不同的访问点,分别拥有各自的 IC 状态。因为引擎关注的不是“所有 .x 的全局统计”,而是“这个位置通常见到什么结构”。
没有 Inline Cache 时,属性访问为什么贵?
如果没有 IC,引擎每次都只能走更通用的属性查找流程。简化后,大致是这样:
- 检查接收者是否合法
- 获取对象当前的内部结构信息
- 看对象自身是否有目标属性
- 如果没有,沿着原型链继续查找
- 如果命中 getter,还要进入调用逻辑
- 如果对象处于字典模式或异常结构,还要进入更复杂的分支
它最大的问题不是“单次一定特别慢”,而是:
即使这个访问点已经执行了几百万次,而且模式非常稳定,它也不会自动变得更便宜。
IC 的价值就在这里。它利用的是一个很现实的事实:
动态语言的执行轨迹,往往具有很强的局部稳定性。
Inline Cache 依赖什么信息?
要理解 IC,必须先知道一个配套概念:对象结构。
不同引擎叫法不同:
- V8 常说
Hidden Class或内部的Map - JavaScriptCore 常说
Structure - SpiderMonkey 常说
Shape
虽然名字不同,但核心思想接近:具有相同属性布局的一类对象,可以共享同一种内部结构描述。
例如:
1
2
const a = { x: 1, y: 2 };
const b = { x: 3, y: 4 };
如果它们的属性集合和添加顺序一致,那么它们在引擎内部很可能共享同一种结构。
于是 obj.x 就不一定非得靠“按名字查找”。它可以被特化成:
if (obj.shape === S1) {
return load_from_slot(obj, slot_for_x)
}
return slow_path()
所以 IC 真正缓存的是:
shape -> 如何读取 x
而不是:
shape -> x 的具体值
这一区别非常重要。
关于 Hidden Class 本身是什么、它为什么会出现、它和 IC 如何协作,可以继续看姊妹篇《Hidden Class 和 Inline Cache》。这篇先聚焦 IC 自己的工作方式。
一个最小化的执行过程
还是这段代码:
1
2
3
function readX(obj) {
return obj.x;
}
第一次执行
1
readX({ x: 1, y: 2 });
这是一个全新的访问点,IC 还没有反馈信息。引擎通常会:
- 走慢路径完成正常属性查找
- 成功拿到
x - 同时记录这次看到的结构,以及如何快速访问
x
第二次执行
1
readX({ x: 10, y: 20 });
如果这个对象和上次是同一种结构,那么引擎只需要先做一次结构检查:
- 命中则直接读取目标槽位
- 不再执行完整通用查找
第三次执行
1
readX({ x: 7, z: 9 });
如果这次对象结构不同,原来的单一路径就不够用了。于是这个访问点会升级自己的状态,学会处理多种结构。
这就引出了 IC 最常见的状态演化。
IC 的几种典型状态
很多资料都会提到以下几个词:
uninitializedmonomorphicpolymorphicmegamorphic
它们描述的是:某个访问点目前见过多少种对象结构,以及引擎还能不能继续高效特化。
1. Uninitialized
初始状态。这个访问点还没有形成有意义的运行时反馈。
可以理解成:
我还不知道这里通常会来什么对象。
2. Monomorphic
如果这个访问点长期只看见一种结构,就进入 monomorphic。
这是 IC 最喜欢的状态,因为快速路径非常简单:
if (obj.shape === S1) {
return load_from_slot(obj, x_slot)
}
return slow_path()
一个热点访问点如果能长期保持 monomorphic,性能通常会很好。这也是很多“保持对象结构稳定”建议背后的根本原因。
3. Polymorphic
如果这个访问点见到了少量几种不同结构,它就会进入 polymorphic。
快速路径会变成:
if (obj.shape === S1) return fast_load_1()
if (obj.shape === S2) return fast_load_2()
if (obj.shape === S3) return fast_load_3()
return slow_path()
它仍然比完全通用查找更快,因为“少量已知模式”的判断成本,通常远低于完整动态解析。
但它已经开始变复杂,也说明这个访问点没那么稳定了。
4. Megamorphic
如果一个访问点见过太多结构,继续为每种结构写特化逻辑的收益就会下降。此时它会进入 megamorphic。
这通常意味着:
- 这个位置的对象来源太杂
- 数据结构缺乏稳定性
- 引擎很难再从这个点提炼出高价值的快速路径
一旦进入 megamorphic,很多优化空间都会明显缩水。
IC 为什么能提速?
把它压缩成一句话:
它把“每次都做通用动态查找”变成了“先看结构,再走固定路径”。
从成本角度看,这是两种完全不同的执行模型。
没有 IC
- 每次都要进行较完整的属性解析
- 要考虑原型链、访问器、不同表示形式等通用语义
- 热点路径无法从历史执行里获益
有 IC
- 先做一次很便宜的结构检查
- 命中后直接按既定路径读取
- 只在未命中时才回退到慢路径
这会带来两个直接收益。
收益一:解释器阶段就能变快
IC 不必等到优化编译器全面介入才起作用。即使代码仍在解释执行阶段,只要访问点形成稳定反馈,IC 就已经能降低不少开销。
收益二:给 JIT 提供类型反馈
IC 记录的不只是“临时加速信息”,它还是优化编译器的重要输入。
如果某个访问点长期是 monomorphic,JIT 会更有信心做这些事:
- 内联属性读取
- 消除冗余检查
- 跨多条指令做更深的特化
所以 IC 在很多引擎里既是即时优化机制,也是后续优化的重要反馈来源。
Inline Cache 不只用于 obj.x
虽然最常见的讲解都用属性读取举例,但 IC 的应用范围远不止这些。
常见类型包括:
Load IC:属性读取,例如obj.xStore IC:属性写入,例如obj.x = 1Call IC:函数调用或方法调用- 元素访问相关 IC:例如
arr[i]
它们背后的共同点都是:
在一个具体操作点上,观察稳定模式,然后为该模式生成快速路径。
Inline Cache 为什么会退化?
IC 的前提是“局部稳定”。只要稳定性被破坏,IC 的效果就会下降。
常见原因包括:
1. 同一个热点函数接收了太多不同结构的对象
1
2
3
4
5
6
7
8
function readX(obj) {
return obj.x;
}
readX({ x: 1 });
readX({ x: 1, y: 2 });
readX({ a: 0, x: 1, b: 2 });
readX({ x: 1, z: 3 });
这个访问点会从 monomorphic 逐渐走向 polymorphic,甚至 megamorphic。
2. 属性添加顺序不稳定
这两段代码在业务语义上都像“一个有 x 和 y 的对象”,但内部结构未必相同:
1
2
3
4
5
6
7
const a = {};
a.x = 1;
a.y = 2;
const b = {};
b.y = 2;
b.x = 1;
对象结构不统一,IC 的命中率就会下降。
3. 使用 delete
1
delete obj.x;
删除属性会破坏对象结构的稳定性。在很多引擎里,它会让对象进入更难优化的表示形式,导致原先依赖紧凑布局的快速路径失效。
4. 原型链被动态修改
如果属性查找依赖原型链,而原型结构在运行时发生变化,引擎就必须重新审视原先的特化路径。
5. 过度动态的元编程
大量混用完全不同结构的数据、大量结构扰动、运行时修改对象语义,都会直接降低 IC 的收益。
一个常见误解:IC 不是在缓存“业务结果”
这个误解值得单独拎出来,因为它会影响你后面对很多引擎术语的理解。
错误理解:
上一次
obj.x是 10,所以下次直接返回 10。
正确理解:
上一次这个访问点看到的是结构
S1,而对于S1,属性x应该通过某个固定槽位读取。
换句话说,IC 缓存的是:
- 结构信息
- 访问策略
- 快速路径入口
而不是:
- 具体业务值
从系统设计角度看:IC 是“观测 + 特化 + 回退”
如果不想陷入具体引擎术语,可以把 Inline Cache 抽象成 3 步:
1. 观测
运行时观察这个访问点到底看到了哪些模式。
2. 特化
如果模式稳定,就为这些模式准备特化后的快速路径。
3. 回退
如果没有命中快速路径,就回到通用逻辑,并继续积累反馈。
这也是现代动态语言 VM 的典型思路:
不在编译前假装一切都静态,而是在运行时先观察,再基于真实分布做投机优化。
前端工程师为什么值得理解 IC?
即使你不写浏览器引擎,这个概念依然很有价值。
它解释了很多看起来像“玄学”的性能建议
例如:
- 尽量让同类对象以一致方式初始化
- 避免在热点路径频繁增删属性
- 不要让同一个热点函数处理过多形态完全不同的数据
这些建议背后,很多都直接和 IC 命中率有关。
它解释了 JavaScript 为什么能很快
JavaScript 快,不是因为它突然变成了静态语言;而是因为引擎在热点路径上用 IC、shape、JIT 等机制,把部分动态行为局部静态化了。
它是理解更高阶 JIT 话题的入口
比如这些概念:
type feedbackspeculative optimizationdeoptimizationhidden class transition
如果不先理解 IC,这些词很容易变成一堆彼此割裂的名词。
总结
Inline Cache 不是浏览器资源缓存,也不是把属性值记下来。它是 JavaScript 引擎在具体访问点上记录运行时反馈的一种优化机制。它通过记住“这个位置通常会遇到什么结构的对象,以及应该如何快速访问属性”,把原本通用、动态、昂贵的属性查找,转化成一次很便宜的结构判断加一次固定读取。
当访问点长期稳定时,IC 可以停留在 monomorphic 状态,收益非常明显;当对象结构开始发散,它会进入 polymorphic,甚至退化到 megamorphic。它之所以能工作,背后依赖的是引擎对对象结构的内部建模,比如 Hidden Class、Shape、Structure 等机制。
理解 Inline Cache,本质上是在理解现代 JavaScript 引擎的一种核心方法论:
不否认动态性,而是在运行时观察动态性,把其中稳定的部分提炼出来,再把稳定性兑换成性能。