前言
本文主要记录 V8 中 TurboFan 优化编译器和 Deopt 机制的基础学习过程。
在 V8 的执行流程中,Ignition 负责解释执行字节码,Sparkplug 负责快速生成 baseline 机器码,而 TurboFan 负责基于运行时反馈生成更高质量的优化代码。TurboFan 的优化通常建立在一些运行时假设之上,例如对象的 Map 是否稳定、属性访问形态是否稳定、参数类型是否稳定等。
这些假设一旦失效,V8 需要从优化代码回退到解释器能够理解的状态,这个过程就是 Deopt。
本文先通过一个最小实验观察 TurboFan 优化和 wrong map Deopt 现象,为后续继续学习 TurboFan 编译流程、Map 推断、FrameState,以及 CVE-2020-6418 做铺垫。
实验环境
本文使用的 V8 环境是单独搭建的旧版本环境:
1
| /home/x2n/Documents/v8-cve-2020-6418/v8
|
对应的 V8 版本为:
当前源码提交为:
1
| bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07
|
该提交是 CVE-2020-6418 修复提交 fb0a60e15695466621cf65932f9152935d859447 之前的版本。本文暂时不分析漏洞利用细节,只先学习 TurboFan 优化和 Deopt 的基本现象。
实验目标
本文先不直接阅读大量 TurboFan 源码,而是通过一个最小实验观察:
- 函数什么时候被 TurboFan 优化?
- 优化后的代码依赖了什么假设?
- 当假设失败时,V8 如何触发 Deopt?
整体流程可以先理解为:
1 2 3 4 5 6 7 8 9 10 11 12
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 55, "rankSpacing": 55, "wrappingWidth": 320}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:220px'>JavaScript 源码</div>"] --> B["<div style='min-width:220px'>Ignition 生成 Bytecode</div>"] B --> C["<div style='min-width:220px'>执行函数并收集反馈</div>"] C --> D["<div style='min-width:260px'>FeedbackVector / Inline Cache</div>"] D --> E["<div style='min-width:260px'>函数变热或被手动标记优化</div>"] E --> F["<div style='min-width:220px'>TurboFan 编译优化代码</div>"] F --> G["<div style='min-width:220px'>Optimized Code</div>"] G --> H{"<div style='min-width:200px'>优化假设是否成立?</div>"} H -- "成立" --> I["<div style='min-width:220px'>继续执行快速路径</div>"] H -- "失败" --> J["<div style='min-width:180px'>触发 Deopt</div>"] J --> K["<div style='min-width:220px'>回到 Ignition 继续执行</div>"]
|
这里的 Inline Cache 简称 IC,中文一般叫“内联缓存”。它不是 CPU cache 那种硬件缓存,而是 V8 在运行时为属性访问、函数调用、二元运算等操作记录反馈信息的机制。后面看到的 LoadProperty MONOMORPHIC、LoadProperty POLYMORPHIC,都可以理解为某个属性读取点的 IC 状态。
实验代码
创建测试文件:
1 2
| cd /home/x2n/Documents/v8-cve-2020-6418 vim testjs/turbofan-deopt-01.js
|
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function f(o) { return o.x + 1; }
let a = {x: 1}; let b = {x: 2}; let c = {y: 3};
%PrepareFunctionForOptimization(f); f(a); f(b); %OptimizeFunctionOnNextCall(f); print(f(a)); print(f(c));
|
这里的重点是:
a 和 b 都有属性 x
c 没有属性 x,只有属性 y
a 和 b 的对象形状比较接近
c 的对象形状和前两个对象不同
在 V8 中,对象的形状通常会通过 Map 描述。TurboFan 可能会根据之前收集到的反馈信息,认为参数 o 大概率会保持同一种对象形状。
运行命令
使用 d8 运行测试:
1 2 3 4 5 6 7
| cd /home/x2n/Documents/v8-cve-2020-6418
./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --trace-opt \ --trace-deopt \ testjs/turbofan-deopt-01.js
|
参数含义:
--allow-natives-syntax:允许使用 %PrepareFunctionForOptimization、%OptimizeFunctionOnNextCall 这类 V8 内部调试函数
--trace-opt:打印函数被优化的日志
--trace-deopt:打印函数发生 Deopt 的日志
执行流程
这段代码的执行过程可以画成下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| %%{init: {"sequence": {"actorMargin": 70, "messageMargin": 55, "width": 180, "boxTextMargin": 8}, "themeVariables": {"fontSize": "15px"}}}%% sequenceDiagram participant JS as JavaScript 代码 participant V8 as V8 participant TF as TurboFan participant DEOPT as Deopt
JS->>V8: %PrepareFunctionForOptimization(f) JS->>V8: f(a) JS->>V8: f(b) V8->>V8: 收集 o.x 的运行时反馈 JS->>V8: %OptimizeFunctionOnNextCall(f) JS->>V8: f(a) V8->>TF: 使用 TurboFan 编译 f TF-->>V8: 生成 optimized code V8-->>JS: 输出 2 JS->>V8: f(c) V8->>V8: 优化代码检查对象 Map V8->>DEOPT: Map 不匹配,触发 wrong map DEOPT-->>V8: 恢复解释器帧 V8-->>JS: 按 JavaScript 语义继续执行,输出 NaN
|
关键输出
运行后可以看到如下关键日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| x2n@x2n:~/Documents/v8-cve-2020-6418$ ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --trace-opt \ --trace-deopt \ testjs/turbofan-deopt-01.js
[manually marking 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> for non-concurrent optimization] [compiling method 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> using TurboFan] [optimizing 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> - took 7.899, 7.852, 0.518 ms] 2 [deoptimizing (DEOPT eager): begin 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> (opt #0) @1, FP to SP delta: 24, caller sp: 0x7fff0706aa90] ;;; deoptimize at <testjs/turbofan-deopt-01.js:2:12>, wrong map reading input frame f => bytecode_offset=0, args=2, height=0, retval=0(#0); inputs: 0: 0x0645082502b1 ; [fp - 16] 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> 1: 0x0645080c0ddd ; [fp + 24] 0x0645080c0ddd <JSGlobal Object> 2: 0x0645080c5e9d ; rcx 0x0645080c5e9d <Object map = 0x64508284ea1> 3: 0x064508250295 ; [fp - 24] 0x064508250295 <ScriptContext[5]> 4: 0x064508040815 ; (literal 2) 0x064508040815 <Odd Oddball: optimized_out> translating interpreted frame f => bytecode_offset=0, variable_frame_size=8, frame_size=72 0x7fff0706aa88: [top + 64] <- 0x0645080c0ddd <JSGlobal Object> ; stack parameter (input #1) 0x7fff0706aa80: [top + 56] <- 0x0645080c5e9d <Object map = 0x64508284ea1> ; stack parameter (input #2) ------------------------- 0x7fff0706aa78: [top + 48] <- 0x74f506b91a91 ; caller's pc 0x7fff0706aa70: [top + 40] <- 0x7fff0706aad0 ; caller's fp 0x7fff0706aa68: [top + 32] <- 0x064508250295 <ScriptContext[5]> ; context (input #3) 0x7fff0706aa60: [top + 24] <- 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> ; function (input #0) 0x7fff0706aa58: [top + 16] <- 0x064508250385 <BytecodeArray[8]> ; bytecode array 0x7fff0706aa50: [top + 8] <- 0x000000000042 <Smi 33> ; bytecode offset ------------------------- 0x7fff0706aa48: [top + 0] <- 0x064508040815 <Odd Oddball: optimized_out> ; accumulator (input #4) [deoptimizing (eager): end 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> @1 => node=0, pc=0x74f506b922a0, caller sp=0x7fff0706aa90, took 0.458 ms] NaN
|
这些输出说明:
f 被手动标记为需要优化
- V8 使用 TurboFan 编译
f
- TurboFan 成功生成优化代码
print(f(a)) 输出 2
print(f(c)) 触发 wrong map Deopt
- Deopt 后程序继续按照 JavaScript 语义执行,最终输出
NaN
日志解析
优化日志
第一段日志:
1
| [manually marking 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> for non-concurrent optimization]
|
这是因为测试代码中调用了:
1
| %OptimizeFunctionOnNextCall(f);
|
它告诉 V8:下一次调用 f 时,尝试对该函数进行优化编译。
随后出现:
1
| [compiling method 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> using TurboFan]
|
这说明 f 被交给 TurboFan 编译。TurboFan 会结合函数 bytecode 和之前收集到的反馈信息,生成优化后的机器码。
再后面:
1
| [optimizing 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> - took 7.899, 7.852, 0.518 ms]
|
表示 TurboFan 已经完成优化编译。
输出 2 的原因
在执行:
时,a.x 的值是 1,所以:
结果为:
这一次调用发生在 f 被标记优化之后,因此 V8 会尝试对 f 进行 TurboFan 优化。
wrong map 的原因
关键日志是:
1
| ;;; deoptimize at <testjs/turbofan-deopt-01.js:2:12>, wrong map
|
源码第 2 行是:
也就是说,Deopt 发生在读取 o.x 附近。
前面 f(a)、f(b) 的调用让 V8 收集到一种反馈:参数 o 看起来像是拥有属性 x 的对象。于是 TurboFan 可以生成一条更快的属性读取路径。
但是这条快速路径通常不是无条件执行的,它依赖一个假设:
当执行:
时,c 是:
它和 {x: 1}、{x: 2} 的对象形状不同,因此 Map 不同。优化代码中的 Map 检查失败,V8 不能继续相信当前优化代码,于是触发 Deopt。
这就是 wrong map 的含义。
可以把这个过程理解为:
1 2 3 4 5 6 7 8 9 10
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 55, "rankSpacing": 55, "wrappingWidth": 340}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:220px'>f(a), f(b)</div>"] --> B["<div style='min-width:280px'>反馈显示参数 o 通常拥有属性 x</div>"] B --> C["<div style='min-width:300px'>TurboFan 为 o.x 生成快速属性读取路径</div>"] C --> D["<div style='min-width:240px'>优化代码中保留 Map Check</div>"] D --> E["<div style='min-width:220px'>f(c) 传入 {y: 3}</div>"] E --> F{"<div style='min-width:220px'>对象 Map 是否匹配?</div>"} F -- "匹配" --> G["<div style='min-width:220px'>继续执行优化代码</div>"] F -- "不匹配" --> H["<div style='min-width:240px'>触发 wrong map Deopt</div>"] H --> I["<div style='min-width:220px'>回到解释器继续执行</div>"]
|
输出 NaN 的原因
Deopt 并不代表程序崩溃,也不代表 JavaScript 语义错误。
Deopt 的作用是:当优化代码的假设失败时,V8 回退到解释器可以理解的执行状态,然后继续按照普通 JavaScript 语义运行。
对于:
因为 c 没有 x 属性,所以:
结果是:
因此:
结果就是:
Deopt 的作用
Deopt 的核心作用是保证语义正确性。
优化代码为了性能,会省略一些通用路径,只保留更快的执行路径。但是这些快速路径依赖运行时假设。当假设失效时,V8 不能继续执行当前优化代码,而是需要恢复到解释器能够理解的状态。
这个恢复过程依赖 TurboFan 编译时记录的 deopt metadata,例如 FrameState。这些信息可以帮助 V8 把 optimized frame 转换回 interpreted frame。
1 2 3 4 5 6 7 8 9
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 55, "rankSpacing": 55, "wrappingWidth": 360}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:260px'>Optimized Code 正在执行</div>"] --> B["<div style='min-width:260px'>Guard / Map Check 失败</div>"] B --> C["<div style='min-width:220px'>触发 eager deopt</div>"] C --> D["<div style='min-width:260px'>读取 optimized frame</div>"] D --> E["<div style='min-width:340px'>根据 FrameState 和 deopt metadata 翻译状态</div>"] E --> F["<div style='min-width:260px'>恢复 interpreted frame</div>"] F --> G["<div style='min-width:220px'>回到 Ignition</div>"] G --> H["<div style='min-width:260px'>继续执行 JavaScript 语义</div>"]
|
在本次实验的日志中,可以看到类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| reading input frame f => bytecode_offset=0, args=2, height=0, retval=0(#0); inputs: 0: 0x0645082502b1 ; [fp - 16] 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> 1: 0x0645080c0ddd ; [fp + 24] 0x0645080c0ddd <JSGlobal Object> 2: 0x0645080c5e9d ; rcx 0x0645080c5e9d <Object map = 0x64508284ea1> 3: 0x064508250295 ; [fp - 24] 0x064508250295 <ScriptContext[5]> 4: 0x064508040815 ; (literal 2) 0x064508040815 <Odd Oddball: optimized_out> translating interpreted frame f => bytecode_offset=0, variable_frame_size=8, frame_size=72 0x7fff0706aa88: [top + 64] <- 0x0645080c0ddd <JSGlobal Object> ; stack parameter (input #1) 0x7fff0706aa80: [top + 56] <- 0x0645080c5e9d <Object map = 0x64508284ea1> ; stack parameter (input #2) ------------------------- 0x7fff0706aa78: [top + 48] <- 0x74f506b91a91 ; caller's pc 0x7fff0706aa70: [top + 40] <- 0x7fff0706aad0 ; caller's fp 0x7fff0706aa68: [top + 32] <- 0x064508250295 <ScriptContext[5]> ; context (input #3) 0x7fff0706aa60: [top + 24] <- 0x0645082502b1 <JSFunction f (sfi = 0x645082500bd)> ; function (input #0) 0x7fff0706aa58: [top + 16] <- 0x064508250385 <BytecodeArray[8]> ; bytecode array 0x7fff0706aa50: [top + 8] <- 0x000000000042 <Smi 33> ; bytecode offset ------------------------- 0x7fff0706aa48: [top + 0] <- 0x064508040815 <Odd Oddball: optimized_out> ; accumulator (input #4)
|
这说明 V8 正在把优化代码中的执行现场翻译成解释器帧,然后回到 Ignition 继续执行。
从 Bytecode 看属性访问反馈
前面的实验已经观察到 wrong map Deopt。接下来需要继续理解一个问题:
1
| TurboFan 是怎么知道参数对象之前是什么形状的?
|
这就需要往前看 Ignition 执行阶段的 bytecode 和反馈信息。
测试代码
创建一个更聚焦的属性读取测试:
1 2 3 4 5 6 7 8 9 10 11
| function f(o) { return o.x; }
let a = {x: 1}; let b = {x: 2}; let c = {x: 3, y: 4};
f(a); f(b); f(c);
|
这里想观察的是 o.x 这个属性读取点,在 Ignition bytecode 里会被表示成什么。
trace-ic 没有输出
先尝试使用:
1 2 3 4
| ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --trace-ic \ testjs/feedback-map-01.js
|
这次运行没有明显输出。这个现象暂时不影响继续分析,因为不同 V8 版本中 IC 相关日志不一定都会直接打印到 stdout,也可能需要配合其他 log 参数。
当前阶段可以先使用 --print-bytecode 观察 Ignition 生成的字节码。
需要注意,文件名应该是:
1
| testjs/feedback-map-01.js
|
如果写成 testjs/feedback-map-01.jss,末尾多了一个 s,就不是同一个测试文件。
print-bytecode 输出
使用下面的命令打印 bytecode:
1 2 3 4
| ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --print-bytecode \ testjs/feedback-map-01.js
|
输出中最重要的是 function f 这一段:
1 2 3 4 5 6 7 8 9 10 11
| [generated bytecode for function: f (0x26b208250025 <SharedFunctionInfo f>)] Parameter count 2 Register count 0 Frame size 0 0x26b20825029a @ 0 : 28 02 00 00 LdaNamedProperty a0, [0], [0] 0x26b20825029e @ 4 : ab Return Constant pool (size = 1) 0x26b20825026d: [FixedArray] in OldSpace - map: 0x26b2080404b1 <Map> - length: 1 0: 0x26b20824ff81 <String[#1]: x>
|
其中最关键的一行是:
1
| LdaNamedProperty a0, [0], [0]
|
它对应源码中的:
LdaNamedProperty 的含义
可以把这条 bytecode 拆开理解:
1
| LdaNamedProperty a0, [0], [0]
|
含义大致是:
LdaNamedProperty:读取一个命名属性
a0:从函数第一个参数上读取属性,也就是参数 o
- 第一个
[0]:属性名在 Constant Pool 中的索引
- 第二个
[0]:这个属性读取点对应的 feedback slot
Constant Pool 中可以看到:
所以第一个 [0] 对应的属性名就是:
因此:
1
| LdaNamedProperty a0, [0], [0]
|
可以理解为:
1
| 从参数 o 上读取属性 x,并把这个属性读取点的运行时反馈记录到 feedback slot 0。
|
属性读取与 Feedback 的关系
这一点非常关键。o.x 并不是简单地读取一次属性就结束了。
在 Ignition 执行阶段,V8 会通过 bytecode 中关联的 feedback slot 记录这个属性访问点的运行时信息,例如:
- 这个位置之前读过哪些对象形状
- 对象的 Map 是否稳定
- 属性访问是 monomorphic、polymorphic 还是 megamorphic
- 后续 TurboFan 是否可以基于这些信息生成快速路径
可以把关系画成:
1 2 3 4 5 6 7
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 60, "rankSpacing": 60, "wrappingWidth": 360}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:220px'>源码:return o.x</div>"] --> B["<div style='min-width:320px'>Bytecode:LdaNamedProperty a0, [0], [0]</div>"] B --> C["<div style='min-width:260px'>Constant Pool [0]:属性名 x</div>"] B --> D["<div style='min-width:280px'>Feedback Slot [0]:记录属性访问反馈</div>"] D --> E["<div style='min-width:300px'>记录对象 Map / IC 状态</div>"] E --> F["<div style='min-width:320px'>TurboFan 后续基于反馈进行优化</div>"]
|
因此,TurboFan 后续优化 o.x 时,并不是凭空猜测对象形状,而是会读取 Ignition 执行过程中积累下来的反馈。
顶层脚本 bytecode
--print-bytecode 输出中还有一大段匿名函数的 bytecode,它对应的是顶层脚本逻辑,例如:
1 2
| CreateObjectLiteral CallUndefinedReceiver1
|
这些指令主要负责:
1 2 3 4
| 创建对象字面量 a、b、c 调用 f(a) 调用 f(b) 调用 f(c)
|
当前阶段暂时不需要细读顶层脚本的每一条 bytecode。重点先关注 function f 中的:
1
| LdaNamedProperty a0, [0], [0]
|
因为它就是后续理解 FeedbackVector、Inline Cache、Map 推断和 TurboFan 属性访问优化的入口。
当前小结
通过这个 bytecode 实验,可以得到一个更明确的结论:
1 2 3 4
| Ignition 在执行 o.x 时,会生成 LdaNamedProperty 字节码。 这个字节码不仅负责读取属性,还关联一个 feedback slot。 该 feedback slot 会记录这个属性访问点的运行时反馈。 TurboFan 后续优化属性读取时,会依赖这些反馈信息。
|
从 FeedbackVector 看 MONOMORPHIC 和 POLYMORPHIC
前面已经知道 LdaNamedProperty a0, [0], [0] 中的第二个 [0] 表示 feedback slot。接下来继续观察这个 slot 在函数执行之后到底记录了什么。
测试代码
测试文件为:
1
| testjs/map-feedback-02.js
|
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function f(o) { return o.x; }
let a = {x: 1}; let b = {x: 2}; let c = {x: 3, y: 4};
print("a and b same map:", %HaveSameMap(a, b)); print("a and c same map:", %HaveSameMap(a, c));
%PrepareFunctionForOptimization(f);
f(a); f(b); f(a); f(b);
print("after monomorphic calls:"); %DebugPrint(f);
f(c);
print("after polymorphic call:"); %DebugPrint(f);
|
这个实验分成两步:
- 先多次调用
f(a)、f(b),让 o.x 只见到同一种对象 Map
- 再调用一次
f(c),让同一个属性读取点见到另一种 Map
完整调试输出
运行命令:
1 2 3
| ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ testjs/map-feedback-02.js
|
完整输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| x2n@x2n:~/Documents/v8-cve-2020-6418$ ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ testjs/map-feedback-02.js a and b same map: true a and c same map: false after monomorphic calls: DebugPrint: 0x199008250335: [Function] in OldSpace - map: 0x199008280289 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x199008241185 <JSFunction (sfi = 0x1990081c4ef9)> - elements: 0x1990080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x199008250109 <SharedFunctionInfo f> - name: 0x19900824ff61 <String[#1]: f> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x199008250319 <ScriptContext[5]> - code: 0x1990000433e1 <Code BUILTIN InterpreterEntryTrampoline> - interpreted - bytecode: 0x199008250409 <BytecodeArray[5]> - source code: (o) { return o.x; } - properties: 0x1990080406e9 <FixedArray[0]> { #length: 0x1990081c0341 <AccessorInfo> (const accessor descriptor) #name: 0x1990081c02fd <AccessorInfo> (const accessor descriptor) #arguments: 0x1990081c0275 <AccessorInfo> (const accessor descriptor) #caller: 0x1990081c02b9 <AccessorInfo> (const accessor descriptor) #prototype: 0x1990081c0385 <AccessorInfo> (const accessor descriptor) } - feedback vector: 0x199008250441: [FeedbackVector] in OldSpace - map: 0x1990080406b9 <Map> - length: 2 - shared function info: 0x199008250109 <SharedFunctionInfo f> - optimized code/marker: OptimizationMarker::kNone - invocation count: 4 - profiler ticks: 0 - slot #0 LoadProperty MONOMORPHIC { [0]: [weak] 0x199008284e79 <Map(HOLEY_ELEMENTS)> [1]: 836 } 0x199008280289: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - constructor - has_prototype_slot - back pointer: 0x19900804030d <undefined> - prototype_validity cell: 0x1990081c0451 <Cell value= 1> - instance descriptors (own) #5: 0x19900824132d <DescriptorArray[5]> - prototype: 0x199008241185 <JSFunction (sfi = 0x1990081c4ef9)> - constructor: 0x1990082412b9 <JSFunction Function (sfi = 0x1990081c4ffd)> - dependent code: 0x1990080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
after polymorphic call: DebugPrint: 0x199008250335: [Function] in OldSpace - map: 0x199008280289 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x199008241185 <JSFunction (sfi = 0x1990081c4ef9)> - elements: 0x1990080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x199008250109 <SharedFunctionInfo f> - name: 0x19900824ff61 <String[#1]: f> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x199008250319 <ScriptContext[5]> - code: 0x1990000433e1 <Code BUILTIN InterpreterEntryTrampoline> - interpreted - bytecode: 0x199008250409 <BytecodeArray[5]> - source code: (o) { return o.x; } - properties: 0x1990080406e9 <FixedArray[0]> { #length: 0x1990081c0341 <AccessorInfo> (const accessor descriptor) #name: 0x1990081c02fd <AccessorInfo> (const accessor descriptor) #arguments: 0x1990081c0275 <AccessorInfo> (const accessor descriptor) #caller: 0x1990081c02b9 <AccessorInfo> (const accessor descriptor) #prototype: 0x1990081c0385 <AccessorInfo> (const accessor descriptor) } - feedback vector: 0x199008250441: [FeedbackVector] in OldSpace - map: 0x1990080406b9 <Map> - length: 2 - shared function info: 0x199008250109 <SharedFunctionInfo f> - optimized code/marker: OptimizationMarker::kNone - invocation count: 5 - profiler ticks: 0 - slot #0 LoadProperty POLYMORPHIC { [0]: 0x1990080c5f99 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> [1]: 0x199008043049 <Symbol: (uninitialized_symbol)> } 0x199008280289: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - constructor - has_prototype_slot - back pointer: 0x19900804030d <undefined> - prototype_validity cell: 0x1990081c0451 <Cell value= 1> - instance descriptors (own) #5: 0x19900824132d <DescriptorArray[5]> - prototype: 0x199008241185 <JSFunction (sfi = 0x1990081c4ef9)> - constructor: 0x1990082412b9 <JSFunction Function (sfi = 0x1990081c4ffd)> - dependent code: 0x1990080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
|
a、b 和 c 的 Map 关系
最开始的输出是:
1 2
| a and b same map: true a and c same map: false
|
这说明:
1 2 3
| let a = {x: 1}; let b = {x: 2}; let c = {x: 3, y: 4};
|
其中 a 和 b 的对象形状一致,都是只有一个属性 x,所以它们共享同一个 Map。
而 c 比 a、b 多了一个属性 y,所以 c 的对象形状不同,Map 也不同。
也就是说:
1 2 3
| Map 可以理解为 V8 描述对象形状的结构。 属性布局一样的对象,往往可以共享同一个 Map。 属性布局不同的对象,会使用不同的 Map。
|
MONOMORPHIC 状态
在只调用 f(a)、f(b) 之后,%DebugPrint(f) 中可以看到:
1 2 3 4 5 6
| - feedback vector: 0x199008250441: [FeedbackVector] in OldSpace - invocation count: 4 - slot #0 LoadProperty MONOMORPHIC { [0]: [weak] 0x199008284e79 <Map(HOLEY_ELEMENTS)> [1]: 836 }
|
这说明 f 的 FeedbackVector 已经创建出来了。
其中:
1
| slot #0 LoadProperty MONOMORPHIC
|
对应的就是前面 bytecode 中的:
1
| LdaNamedProperty a0, [0], [0]
|
也就是源码中的:
MONOMORPHIC 表示这个属性读取点目前只见过一种对象 Map。
这里记录的 Map 是:
1
| 0x199008284e79 <Map(HOLEY_ELEMENTS)>
|
这个 Map 对应的就是 a 和 b 共享的对象形状。
此时 V8 对 f(o) 中 o.x 的理解可以概括为:
1 2
| 过去这个属性访问点一直看到同一种对象 Map。 因此这个 LoadProperty slot 处于 MONOMORPHIC 状态。
|
如果后续 TurboFan 基于这个反馈优化 o.x,它就可以生成比较直接的快速路径:
1 2 3
| 检查对象 Map 是否为 0x199008284e79 -> 如果匹配,就直接按照该 Map 的属性布局读取 x -> 如果不匹配,就不能继续相信这条快速路径
|
POLYMORPHIC 状态
在继续调用一次:
之后,再次 %DebugPrint(f),可以看到同一个 slot 发生了变化:
1 2 3 4 5
| - invocation count: 5 - slot #0 LoadProperty POLYMORPHIC { [0]: 0x1990080c5f99 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> [1]: 0x199008043049 <Symbol: (uninitialized_symbol)> }
|
状态从:
变成了:
原因是 c 的 Map 和 a、b 不同。虽然 c 也有属性 x,但它的对象形状不一样:
因此,同一个属性读取点 o.x 现在见过多种 Map,FeedbackVector 就把这个 slot 从单态变成多态。
进入 POLYMORPHIC 后,slot 中不再直接保存一个 Map,而是保存:
1
| 0x1990080c5f99 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
|
可以把它理解为一个弱引用数组,用来记录多组 Map / handler 信息。也就是说,多态状态下,V8 需要保存的不再是“一个对象形状”,而是一组可能见过的对象形状。
MONOMORPHIC、POLYMORPHIC、MEGAMORPHIC
目前可以先这样理解这几个状态:
1 2 3 4 5 6 7 8
| MONOMORPHIC: 一个属性访问点只见过一种 Map。
POLYMORPHIC: 一个属性访问点见过多种 Map,但数量还不算太多。
MEGAMORPHIC: 一个属性访问点见过太多种 Map,V8 不再为少数 Map 做精确记录。
|
它们和 TurboFan 优化的关系是:
1 2 3 4 5 6 7 8 9 10
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 60, "rankSpacing": 60, "wrappingWidth": 380}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:280px'>LdaNamedProperty 关联 feedback slot</div>"] --> B["<div style='min-width:260px'>FeedbackVector 记录 IC 状态</div>"] B --> C{"<div style='min-width:260px'>这个访问点见过几个 Map?</div>"} C -- "一个 Map" --> D["<div style='min-width:240px'>MONOMORPHIC</div>"] C -- "多个 Map" --> E["<div style='min-width:240px'>POLYMORPHIC</div>"] C -- "太多 Map" --> F["<div style='min-width:240px'>MEGAMORPHIC</div>"] D --> G["<div style='min-width:320px'>TurboFan 可生成单一 Map Check 快速路径</div>"] E --> H["<div style='min-width:340px'>TurboFan 可能生成多组 Map Check 快速路径</div>"] F --> I["<div style='min-width:320px'>优化空间变小,更多依赖通用路径</div>"]
|
当前小结
通过这个实验,可以把前面几部分串起来:
1 2 3 4 5 6
| 源码 return o.x -> Ignition 生成 LdaNamedProperty a0, [0], [0] -> feedback slot #0 记录 LoadProperty 的运行时反馈 -> f(a)、f(b) 只传入同一种 Map,slot #0 为 MONOMORPHIC -> f(c) 传入不同 Map,slot #0 变成 POLYMORPHIC -> TurboFan 后续优化 o.x 时会依赖这些 Map 反馈
|
这也解释了前面的 wrong map Deopt:
1 2
| 优化代码基于已有 Map 反馈生成快速路径。 当运行时传入一个不符合优化假设的 Map 时,就会触发 wrong map Deopt。
|
从 MONOMORPHIC Feedback 到 wrong map Deopt
前面已经分别观察了两个现象:
LdaNamedProperty 会关联 feedback slot
LoadProperty 的 feedback slot 可以从 MONOMORPHIC 变成 POLYMORPHIC
接下来继续把这两部分和 TurboFan 优化连接起来:
1 2 3 4 5
| MONOMORPHIC Map 反馈 -> TurboFan 基于该 Map 生成优化代码 -> 传入不同 Map 的对象 -> 优化假设失败 -> 触发 wrong map Deopt
|
测试代码
测试文件为:
1
| testjs/map-feedback-deopt-03.js
|
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function f(o) { return o.x + 1; }
let a = {x: 1}; let b = {x: 2}; let c = {x: 3, y: 4};
print("a and b same map:", %HaveSameMap(a, b)); print("a and c same map:", %HaveSameMap(a, c));
%PrepareFunctionForOptimization(f);
f(a); f(b); f(a); f(b);
print("before optimization:"); %DebugPrint(f);
%OptimizeFunctionOnNextCall(f);
print("optimized call result:", f(a));
print("after optimization:"); %DebugPrint(f);
print("different map call result:", f(c));
print("after different map:"); %DebugPrint(f);
|
这个实验和前面 return o.x 的实验相比,多了 + 1。因此 FeedbackVector 中不仅会记录属性读取反馈,还会记录二元运算反馈。
运行命令
1 2 3 4 5 6 7
| cd /home/x2n/Documents/v8-cve-2020-6418
./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --trace-opt \ --trace-deopt \ testjs/map-feedback-deopt-03.js
|
完整调试输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
| x2n@x2n:~/Documents/v8-cve-2020-6418$ ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --trace-opt \ --trace-deopt \ testjs/map-feedback-deopt-03.js a and b same map: true a and c same map: false before optimization: DebugPrint: 0x47308250511: [Function] in OldSpace - map: 0x047308280289 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - elements: 0x0473080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x047308250221 <SharedFunctionInfo f> - name: 0x04730824ffed <String[#1]: f> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x0473082504f5 <ScriptContext[5]> - code: 0x0473000433e1 <Code BUILTIN InterpreterEntryTrampoline> - interpreted - bytecode: 0x0473082505e5 <BytecodeArray[8]> - source code: (o) { return o.x + 1; } - properties: 0x0473080406e9 <FixedArray[0]> { #length: 0x0473081c0341 <AccessorInfo> (const accessor descriptor) #name: 0x0473081c02fd <AccessorInfo> (const accessor descriptor) #arguments: 0x0473081c0275 <AccessorInfo> (const accessor descriptor) #caller: 0x0473081c02b9 <AccessorInfo> (const accessor descriptor) #prototype: 0x0473081c0385 <AccessorInfo> (const accessor descriptor) } - feedback vector: 0x47308250631: [FeedbackVector] in OldSpace - map: 0x0473080406b9 <Map> - length: 3 - shared function info: 0x047308250221 <SharedFunctionInfo f> - optimized code/marker: OptimizationMarker::kNone - invocation count: 4 - profiler ticks: 0 - slot #0 BinaryOp BinaryOp:SignedSmall { [0]: 1 } - slot #1 LoadProperty MONOMORPHIC { [1]: [weak] 0x047308284e79 <Map(HOLEY_ELEMENTS)> [2]: 836 } 0x47308280289: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - constructor - has_prototype_slot - back pointer: 0x04730804030d <undefined> - prototype_validity cell: 0x0473081c0451 <Cell value= 1> - instance descriptors (own) #5: 0x04730824132d <DescriptorArray[5]> - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - constructor: 0x0473082412b9 <JSFunction Function (sfi = 0x473081c4ffd)> - dependent code: 0x0473080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
[manually marking 0x047308250511 <JSFunction f (sfi = 0x47308250221)> for non-concurrent optimization] [compiling method 0x047308250511 <JSFunction f (sfi = 0x47308250221)> using TurboFan] [optimizing 0x047308250511 <JSFunction f (sfi = 0x47308250221)> - took 93.060, 52.100, 0.982 ms] optimized call result: 2 after optimization: DebugPrint: 0x47308250511: [Function] in OldSpace - map: 0x047308280289 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - elements: 0x0473080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x047308250221 <SharedFunctionInfo f> - name: 0x04730824ffed <String[#1]: f> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x0473082504f5 <ScriptContext[5]> - code: 0x047300082ae1 <Code OPTIMIZED_FUNCTION> - source code: (o) { return o.x + 1; } - properties: 0x0473080406e9 <FixedArray[0]> { #length: 0x0473081c0341 <AccessorInfo> (const accessor descriptor) #name: 0x0473081c02fd <AccessorInfo> (const accessor descriptor) #arguments: 0x0473081c0275 <AccessorInfo> (const accessor descriptor) #caller: 0x0473081c02b9 <AccessorInfo> (const accessor descriptor) #prototype: 0x0473081c0385 <AccessorInfo> (const accessor descriptor) } - feedback vector: 0x47308250631: [FeedbackVector] in OldSpace - map: 0x0473080406b9 <Map> - length: 3 - shared function info: 0x047308250221 <SharedFunctionInfo f> - optimized code/marker: OptimizationMarker::kNone - invocation count: 4 - profiler ticks: 0 - slot #0 BinaryOp BinaryOp:SignedSmall { [0]: 1 } - slot #1 LoadProperty MONOMORPHIC { [1]: [weak] 0x047308284e79 <Map(HOLEY_ELEMENTS)> [2]: 836 } 0x47308280289: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - constructor - has_prototype_slot - back pointer: 0x04730804030d <undefined> - prototype_validity cell: 0x0473081c0451 <Cell value= 1> - instance descriptors (own) #5: 0x04730824132d <DescriptorArray[5]> - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - constructor: 0x0473082412b9 <JSFunction Function (sfi = 0x473081c4ffd)> - dependent code: 0x0473080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
[deoptimizing (DEOPT eager): begin 0x047308250511 <JSFunction f (sfi = 0x47308250221)> (opt #0) @1, FP to SP delta: 24, caller sp: 0x7ffd87fc68c8] ;;; deoptimize at <testjs/map-feedback-deopt-03.js:2:12>, wrong map reading input frame f => bytecode_offset=0, args=2, height=0, retval=0(#0); inputs: 0: 0x047308250511 ; [fp - 16] 0x047308250511 <JSFunction f (sfi = 0x47308250221)> 1: 0x0473080c0ddd ; [fp + 24] 0x0473080c0ddd <JSGlobal Object> 2: 0x0473080c5fe1 ; rcx 0x0473080c5fe1 <Object map = 0x47308284ec9> 3: 0x0473082504f5 ; [fp - 24] 0x0473082504f5 <ScriptContext[5]> 4: 0x047308040815 ; (literal 2) 0x047308040815 <Odd Oddball: optimized_out> translating interpreted frame f => bytecode_offset=0, variable_frame_size=8, frame_size=72 0x7ffd87fc68c0: [top + 64] <- 0x0473080c0ddd <JSGlobal Object> ; stack parameter (input #1) 0x7ffd87fc68b8: [top + 56] <- 0x0473080c5fe1 <Object map = 0x47308284ec9> ; stack parameter (input #2) ------------------------- 0x7ffd87fc68b0: [top + 48] <- 0x7ba270791a91 ; caller's pc 0x7ffd87fc68a8: [top + 40] <- 0x7ffd87fc6910 ; caller's fp 0x7ffd87fc68a0: [top + 32] <- 0x0473082504f5 <ScriptContext[5]> ; context (input #3) 0x7ffd87fc6898: [top + 24] <- 0x047308250511 <JSFunction f (sfi = 0x47308250221)> ; function (input #0) 0x7ffd87fc6890: [top + 16] <- 0x0473082505e5 <BytecodeArray[8]> ; bytecode array 0x7ffd87fc6888: [top + 8] <- 0x000000000042 <Smi 33> ; bytecode offset ------------------------- 0x7ffd87fc6880: [top + 0] <- 0x047308040815 <Odd Oddball: optimized_out> ; accumulator (input #4) [deoptimizing (eager): end 0x047308250511 <JSFunction f (sfi = 0x47308250221)> @1 => node=0, pc=0x7ba2707922a0, caller sp=0x7ffd87fc68c8, took 0.686 ms] different map call result: 4 after different map: DebugPrint: 0x47308250511: [Function] in OldSpace - map: 0x047308280289 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - elements: 0x0473080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x047308250221 <SharedFunctionInfo f> - name: 0x04730824ffed <String[#1]: f> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x0473082504f5 <ScriptContext[5]> - code: 0x047300082ae1 <Code OPTIMIZED_FUNCTION> - interpreted - bytecode: 0x0473082505e5 <BytecodeArray[8]> - source code: (o) { return o.x + 1; } - properties: 0x0473080406e9 <FixedArray[0]> { #length: 0x0473081c0341 <AccessorInfo> (const accessor descriptor) #name: 0x0473081c02fd <AccessorInfo> (const accessor descriptor) #arguments: 0x0473081c0275 <AccessorInfo> (const accessor descriptor) #caller: 0x0473081c02b9 <AccessorInfo> (const accessor descriptor) #prototype: 0x0473081c0385 <AccessorInfo> (const accessor descriptor) } - feedback vector: 0x47308250631: [FeedbackVector] in OldSpace - map: 0x0473080406b9 <Map> - length: 3 - shared function info: 0x047308250221 <SharedFunctionInfo f> - optimized code/marker: OptimizationMarker::kNone - invocation count: 4 - profiler ticks: 0 - slot #0 BinaryOp BinaryOp:SignedSmall { [0]: 1 } - slot #1 LoadProperty POLYMORPHIC { [1]: 0x0473080c6051 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> [2]: 0x047308043049 <Symbol: (uninitialized_symbol)> } 0x47308280289: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - constructor - has_prototype_slot - back pointer: 0x04730804030d <undefined> - prototype_validity cell: 0x0473081c0451 <Cell value= 1> - instance descriptors (own) #5: 0x04730824132d <DescriptorArray[5]> - prototype: 0x047308241185 <JSFunction (sfi = 0x473081c4ef9)> - constructor: 0x0473082412b9 <JSFunction Function (sfi = 0x473081c4ffd)> - dependent code: 0x0473080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
|
Bytecode 与 slot 对应关系
为了确认 FeedbackVector 中 slot 的含义,可以只打印 f 的 bytecode:
1 2 3 4 5
| ./v8/out.gn/x64.cve.debug/d8 \ --allow-natives-syntax \ --print-bytecode \ --print-bytecode-filter=f \ testjs/map-feedback-deopt-03.js
|
输出中 f 的 bytecode 为:
1 2 3 4 5 6 7 8 9 10 11 12
| [generated bytecode for function: f (0x3c4c08250195 <SharedFunctionInfo f>)] Parameter count 2 Register count 0 Frame size 0 0x3c4c08250512 @ 0 : 28 02 00 01 LdaNamedProperty a0, [0], [1] 0x3c4c08250516 @ 4 : 40 01 00 AddSmi [1], [0] 0x3c4c08250519 @ 7 : ab Return Constant pool (size = 1) 0x3c4c082504e5: [FixedArray] in OldSpace - map: 0x3c4c080404b1 <Map> - length: 1 0: 0x3c4c0824ff81 <String[#1]: x>
|
这里需要注意 slot 编号:
1 2
| LdaNamedProperty a0, [0], [1] AddSmi [1], [0]
|
对应关系是:
1 2
| slot #1:LoadProperty,也就是 o.x slot #0:BinaryOp,也就是 + 1
|
因此,在这个实验中,属性读取反馈不是 slot #0,而是 slot #1。
优化前:LoadProperty 处于 MONOMORPHIC
优化前的 FeedbackVector 中可以看到:
1 2 3 4 5 6 7 8 9 10 11
| - code: 0x0473000433e1 <Code BUILTIN InterpreterEntryTrampoline> - interpreted - feedback vector: 0x47308250631: [FeedbackVector] in OldSpace - invocation count: 4 - slot #0 BinaryOp BinaryOp:SignedSmall { [0]: 1 } - slot #1 LoadProperty MONOMORPHIC { [1]: [weak] 0x047308284e79 <Map(HOLEY_ELEMENTS)> [2]: 836 }
|
此时 f 仍然运行在解释器入口:
1
| <Code BUILTIN InterpreterEntryTrampoline>
|
slot #1 LoadProperty MONOMORPHIC 说明 o.x 这个属性读取点目前只见过一种 Map:
1
| 0x047308284e79 <Map(HOLEY_ELEMENTS)>
|
这个 Map 来自 a 和 b 共享的对象形状。
TurboFan 优化
执行:
1 2
| %OptimizeFunctionOnNextCall(f); print("optimized call result:", f(a));
|
之后,可以看到:
1 2 3 4
| [manually marking 0x047308250511 <JSFunction f (sfi = 0x47308250221)> for non-concurrent optimization] [compiling method 0x047308250511 <JSFunction f (sfi = 0x47308250221)> using TurboFan] [optimizing 0x047308250511 <JSFunction f (sfi = 0x47308250221)> - took 93.060, 52.100, 0.982 ms] optimized call result: 2
|
这说明 f 被 TurboFan 编译,并生成了优化代码。
再次 %DebugPrint(f),可以看到:
1 2 3 4 5
| - code: 0x047300082ae1 <Code OPTIMIZED_FUNCTION> - slot #1 LoadProperty MONOMORPHIC { [1]: [weak] 0x047308284e79 <Map(HOLEY_ELEMENTS)> [2]: 836 }
|
此时 f 的 code 已经变成:
1
| <Code OPTIMIZED_FUNCTION>
|
但是 slot #1 仍然是 MONOMORPHIC,说明 TurboFan 优化时可以使用这个单态 Map 反馈来生成属性读取快速路径。
传入不同 Map 后触发 wrong map Deopt
接着执行:
1
| print("different map call result:", f(c));
|
日志中出现:
1
| ;;; deoptimize at <testjs/map-feedback-deopt-03.js:2:12>, wrong map
|
Deopt 日志里还可以看到传入对象的 Map:
1
| 2: 0x0473080c5fe1 ; rcx 0x0473080c5fe1 <Object map = 0x47308284ec9>
|
这个 Map 是:
它和优化前 LoadProperty MONOMORPHIC 记录的 Map 不同:
所以优化代码中的 Map Check 失败,触发 wrong map Deopt。
这里需要注意:c 虽然 Map 不同,但它仍然有 x 属性:
因此 Deopt 后回到普通 JavaScript 语义继续执行:
所以最终输出是:
1
| different map call result: 4
|
Deopt 后:LoadProperty 变为 POLYMORPHIC
最后再次 %DebugPrint(f),可以看到:
1 2 3 4
| - slot #1 LoadProperty POLYMORPHIC { [1]: 0x0473080c6051 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> [2]: 0x047308043049 <Symbol: (uninitialized_symbol)> }
|
也就是说,f(c) 执行之后,同一个属性读取点 o.x 从:
1
| LoadProperty MONOMORPHIC
|
变成了:
1
| LoadProperty POLYMORPHIC
|
原因是该访问点现在至少见过两种 Map:
1 2
| a / b 的 Map:0x047308284e79 c 的 Map:0x47308284ec9
|
进入 POLYMORPHIC 后,FeedbackVector 中不再直接保存单个 Map,而是使用:
来记录多组 Map / handler 信息。
当前实验结论
这次实验把 FeedbackVector、TurboFan 和 wrong map Deopt 的关系串起来了:
1 2 3 4 5 6 7 8 9 10
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 60, "rankSpacing": 60, "wrappingWidth": 380}, "themeVariables": {"fontSize": "15px"}}}%% flowchart TD A["<div style='min-width:280px'>f(a), f(b) 收集运行时反馈</div>"] --> B["<div style='min-width:320px'>slot #1 LoadProperty MONOMORPHIC</div>"] B --> C["<div style='min-width:320px'>FeedbackVector 记录单一对象 Map</div>"] C --> D["<div style='min-width:300px'>TurboFan 基于该 Map 优化 o.x</div>"] D --> E["<div style='min-width:300px'>生成 Optimized Code</div>"] E --> F["<div style='min-width:280px'>f(c) 传入不同 Map</div>"] F --> G["<div style='min-width:260px'>Map Check 失败</div>"] G --> H["<div style='min-width:260px'>触发 wrong map Deopt</div>"] H --> I["<div style='min-width:320px'>Feedback 更新为 POLYMORPHIC</div>"]
|
可以总结为:
1 2 3 4 5 6 7 8
| LdaNamedProperty a0, [0], [1] -> slot #1 记录 o.x 的 LoadProperty feedback -> f(a)、f(b) 让 slot #1 处于 MONOMORPHIC -> TurboFan 基于 MONOMORPHIC Map 反馈生成优化代码 -> f(c) 传入不同 Map -> 优化代码触发 wrong map Deopt -> V8 回到解释器语义继续执行 -> FeedbackVector 更新为 POLYMORPHIC
|
TurboFan 优化流程总结
通过这个实验,可以先把 TurboFan 的优化流程理解成:
1 2 3 4 5 6 7 8 9 10 11 12
| %%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 60, "rankSpacing": 65, "wrappingWidth": 300}, "themeVariables": {"fontSize": "15px"}}}%% flowchart LR A["<div style='min-width:170px'>运行 JavaScript</div>"] --> B["<div style='min-width:200px'>Ignition 执行 Bytecode</div>"] B --> C["<div style='min-width:180px'>收集 Feedback</div>"] C --> D["<div style='min-width:180px'>触发优化</div>"] D --> E["<div style='min-width:220px'>TurboFan 构建 IR 图</div>"] E --> F["<div style='min-width:220px'>类型推断 / Map 推断</div>"] F --> G["<div style='min-width:200px'>生成优化机器码</div>"] G --> H["<div style='min-width:180px'>执行快速路径</div>"] H --> I{"<div style='min-width:160px'>假设失败?</div>"} I -- "否" --> H I -- "是" --> J["<div style='min-width:200px'>Deopt 回解释器</div>"]
|
在这个例子里,对应关系是:
1 2 3 4 5 6 7
| f(a)、f(b) -> 收集到对象拥有属性 x 的反馈 -> TurboFan 基于该反馈优化 o.x 读取 -> 优化代码中保留 Map check -> f(c) 传入不同 Map 的对象 -> Map check 失败 -> wrong map deopt
|
小结
TurboFan 优化并不是简单地把 JavaScript 源码翻译成机器码,而是基于运行时反馈做推测性优化。
这些推测通常由 Map check、类型 check 等检查保护。当检查失败时,V8 会通过 Deopt 回退到解释器,恢复普通 JavaScript 语义。
这个实验中最重要的观察是:
1 2
| TurboFan 会相信之前的运行反馈,但这种相信不是无条件的。 当对象 Map 和优化代码预期不一致时,V8 会触发 wrong map deopt。
|
后面继续学习 Deopt 时,需要重点理解:
- FeedbackVector 如何记录运行时反馈
- Map / HiddenClass 如何描述对象形状
- TurboFan 如何根据反馈生成优化代码
- FrameState 如何帮助 V8 从优化代码恢复到解释器状态
- 为什么 JIT 漏洞经常出现在“优化假设被错误维持”的位置