2024-06-30
V8 Inline Caches 优化及其汇编细节
写在前面:前段时间工作重心在性能优化,着重看了 V8 相关的优化细节,尤其是 Inline Caches (ICs) 技术,后面依据这项技术将核心指标性能优化到原来的十倍,这里记录一下 ICs 相关细节,仅供参考
超长文警告

# 解释器 + JIT 的性能比想象中的要好

解释器手写的 line-by-line 虚拟机执行流水线完全比不上 CPU 内部的指令流水线以及更为夸张的分支预测手段,因此我个人过去认为 C 至少比 JS 快 10 倍+,直到最近看了不少的 v8 benchmark 之后才对这块有所改观,目前的观点是:只要构造合适,V8 JIT 可以有媲美原生 AOT 的性能,当然也容易出现非常夸张的性能劣化。(这里当然是指 CPU 性能,内存性能肯定永远比不上裸机的)
回到 V8 的执行过程,它首先将代码编译为 AST,然后将 AST 编译为 bytecode 字节码并交给叫做 ignition 的解释器进行解释执行,并在「合适」的时候将这些字节码经由 TurboFan 编译为裸机能直接运行的机器汇编指令,这种边执行边编译的过程叫做 JIT。
除此之外,引擎在运行期间还会持续收集调用反馈 feedback,并依据这些 feedback 进一步优化 JIT 的效果。
举例来说,下图的 add 函数,在后面调用了非常多次后,引擎就可以合理的假设 (assumption) ——「add 接受的两个参数 “大概率” 都是整数」
00function add(a, b) {
01  return a + b;
02}
03
04for (let i = 0; i < 1_000_000; i++) {
05  // 调用了非常多次后,引擎就可以得到一条重要信息:
06  // add 接受的两个参数 “大概率” 是整数
07  add(i, i);
08}
那么根据这样的假设就可以按这样的方式进行 JIT 优化 (伪代码):
00// 依据这个假设「add 接受的两个参数 “大概率” 都是整数」
01// 来将前面的 js add 优化编译为一个原生的函数实现: 
02fn add_jit(a: unknown, b: unknown): int {
03  if (a 不是整数) goto 回滚到解释器运行;
04  if (b 不是整数) goto 回滚到解释器运行;
05  // 执行汇编级别原生整数加法
06  return X86_ASM_ADD(a, b);
07}

# V8 内置 runtime 指令 --allow-natives-syntax

利用这个参数开启 v8 注入的 runtime call,帮助分析和调试 v8
00# node 下开启
01$ node --allow-natives-syntax
02# chrome 下开启
03$ open -a Chromium --args --js-flags="--allow-natives-syntax"
开启后可以在 js 用 % 开头的内置 runtime 调用来输出一些内容
00%DebugPrint(1234);
01// node --allow-natives-syntax ./test.js
02// 会输出很多东西 ...
下面是一些常用指令

%DebugPrint(something);

可以打印对象在 v8 的内部信息,比如打印一个函数:

%GetOptimizationStatus(fn);

获取函数当前的优化 status,后文会详细介绍:
对应的是 V8 源码里的这个枚举:
从开发视角来看,一个函数最佳的 status 应该是 81 也就是
00000000000001010001
:
loading...

%OptimizeFunctionOnNextCall(fn);

告诉 v8 下次调用主动触发优化函数 fn

%HasFastProperties(obj);

%HasFastProperties 可以用来打印对象是否是 Fast Properties 模式
后文会介绍这个 Fast Properties 和与之对立的 Slow Properties

# V8 基于 assumption 的汇编优化细节

那么,V8 是如何利用「合理的假设」来通过 TurboFan 将代码编译为汇编机器码的呢?我们先来看这个例子,一个 add(x,y) 函数,如果运行期间出现了多种类型的传参,那么会导致代码变慢:
我们可以看到,L15 速度慢了非常多,比一开始的 66ms 慢了几倍,推测原因:
  1. 1.
    一开始只会传数字的时候,V8 会假设这是数字加法,可以极致优化。(66 毫秒可以跑完)
  2. 2.
    L13 传入其他参数,上述假设会被推翻,此时打印一次优化状态可以看看出现了反优化,在 L13 执行的时候实际走的是 ignition 解释器去跑的。
  3. 3.
    执行 L15 for 循环走了足够多次后,turbofan 收集到足够的信息后会重新建立假设来做优化,不过这次的假设是「入参可能是 number 也可能是 string」—— 这意味着调用的时候要多判断入参类型是 string 还是 number 从而导致了最终的性能劣化 (一模一样的代码要 243 毫秒才跑完,慢了有三倍吧)
那么,在汇编层面上,V8 如何区分 0xXXXX 是数字还是对象?—— V8 使用 Tagged Pointer 来表示 JS 里的值和引用,具体见下一节

Tagged Pointer

对于 V8 里的 Tagged Pointer,首先它是 C/C++ 里通用的优化技术,不只在 V8 里有用,具体来说就是依据 pointer 自身的数值的某些位来决定 pointer 的行为,也就是说这类指针的特点是「其指针数值上的某些位有特殊含义」,比如在 v8 里,js 堆指针和 SMI 小整数类型(small intergers)是通过 Tagged Pointer 来表达和引用的,区别就在于最低一位是不是 0 来决定其指针类型:
00# 对象指针(32 位):
01xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx1
02
03# SMI 小整数(32 位)其中 xxx 部分为数值部分:
04xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx0
用 C 表达就是这样:
00#include <stdio.h>
01
02void printTaggedPointer(void * p) {
03  // 强转一下, 关注 p 本身的数值
04  unsigned int tp = ((unsigned int) p);
05
06  if ((tp & 0b1) == 0b0) {
07    printf("p 是 SMI, 数值大小为 0x%x 
08", tp >> 1);
09    return;
10  }
11
12  printf("p 是堆对象指针, Object<0x%x> 
13", tp);
14  // printObject(*p); // 假设有个方法可以打印堆对象
15}
16
17int main() {
18  printTaggedPointer(0x1234 << 1); // smi
19  printTaggedPointer(17); // object
20  return 0;
21}

利用 Tagged Pointer 来验证假设

我为何要提到 Tagged Pointer ? 因为这跟底层汇编 codegen 密切相关,比如前面的 add_jit 里,V8 就是利用 Tagged Pointer 细节来做 codegen 来实现 JIT 优化的:
00// 依据这个假设「add 接受的两个参数 “大概率” 都是整数」
01// 来将前面的 js add JIT 优化编译为一个原生的函数实现: 
02fn add_jit(a: unknown, b: unknown): int {
03  if (a 最低位不是 0) goto 回滚到解释器运行; // 不是 0 则代表不是 SMI 整数
04  if (b 最低位不是 0) goto 回滚到解释器运行; // 不是 0 则代表不是 SMI 整数
05  // 执行汇编级别原生整数加法
06  return X86_ASM_ADD(a, b);
07}

assumption 被打破的时候不会 crash / 硬件错误 / 段错误吗?

前面已经提过了,是可以通过合理的数据结构设计来在汇编的层面上区分入参的类型的,比如利用 Tagged Pointer 来区分整数 / 对象类型 ——
上述说法可能会比较含糊,我们可以具体看看打出来的汇编是咋样的来验证我们刚学的 Tagged Pointer,可以通过以下方式打印出优化后的 x86 汇编(m1 芯片的苹果电脑应该是 arm 汇编)
00$ node --print-opt-code \
01       --allow-natives-syntax \
02       --trace-opt \
03       --trace-deopt \
04       ./a.js
如下图所示,L19 ~ L22 其实就是在根据 Tagged Pointer 的性质来判断入参是不是 SMI,具体来说是
[rbx+0xf]
与 0x1 做按位与操作(
[rbx+0xf]
是通过栈传递的参数,是 v8 里 js 的调用约定)如果结果是 0 则跳转 0x10b7cc34f 即后续的正常流程,否则走到
CompileLazyDeoptimizedCode
走反优化流程,回滚用 ignition 字节码解释器去执行:
另外我们也可以看到,核心逻辑对应到汇编也就一行,剩余的指令要么是 checkpoint 要么是 v8/js 的调用约定,在这么多冗余指令的情况下执行性能依然很快,可见 CPU 指令流水线效率比起 line-by-line 的解释器流水线要高得多了。
而且,从汇编和伪码来看也能知道,如果 assumption 经常被打破,性能会变差,而且如果入参的类型太多也会导致 checkpoint / type-guard 的逻辑变复杂,因此也能解释前面 add(a, b) 为什么打破一次之后性能就不如一开始的性能了 —— 类型检查 checkpoint 逻辑会变得更复杂导致优化效率下降:
00// 依据这个假设「add 接受的两个参数 “大概率” 都是整数」
01// 来将前面的 js add JIT 优化编译为一个原生的函数实现: 
02fn add_jit_2(a: unknown, b: unknown): int {
03  if (a 最低位不是 0) goto 回滚到解释器运行; // 不是 0 则代表不是 SMI 整数
04  if (b 最低位不是 0) goto 回滚到解释器运行; // 不是 0 则代表不是 SMI 整数
05
06  // 如果出现过 add('', '') 打破了先前的假设,那么后续就会多一条这样的检查了
07  if (a 是字符串 && b 是字符串) return X86_ASM_STRING_CONCAT(a, b);
08
09  // ... 其他 checkpoint 比如, boolean + boolean
10  // ... 总之传的类型越杂这里行数就越多 ...
11  // 而对于过于复杂的函数, V8 就不会开启 TurboFan 汇编优化了
12
13  // 执行汇编级别原生整数加法
14  return X86_ASM_ADD(a, b);
15}

哪里可以打印所谓 feedback ?

通过 %DebugPrint 可以看到
当打破这个 assumption 后,会变成 Any

多态 return 会导致优化效果打折吗?

不会, 如图:

feedback slot 里的 monomorphic 是?

一共有三种,代表参数类型的复杂度:
  1. 1.
    Monomorphic 单态:指参数的类型只有一种,不会变
  2. 2.
    Polymorphic 多态:指参数的类型有多种 (比较短的 union type)
  3. 3.
    Megamorphic 巨态:指参数的类型非常复杂 (非常长的 union type)
根据前面提到的 checkpoint,上面三个 mono 的 checkpoint 最少,而最后的 mega 将会非常多,优化性能最差,或者 V8 干脆就不会对这类函数做更深度的机器码优化了(比如后文会提到的 ICs)

TurboFan 过程本身耗时怎么样?

从 JS AST 编译到机器码也需要开销,毫秒级:

反优化太多次怎么办?

根据这篇文章 https://erdem.pl/2019/08/v-8-function-optimization 如果某个函数「反优化」超过 5 次后,v8 以后就不再会对这个函数做优化了,不过我无法复现他说的这个情况,可能是老版本的 v8 的表现,node16 不会这样,不管怎样只要 run 了足够多次都会 turbofanned,只是如果「曾经传的参数类型太 union typed」会导致优化效果出现非常大的折损

什么时候会启动 TutboFan ?

前面我们已经知道了「运行足够多次」会触发优化,而这只是其中一种情况,具体可以参考 v8 里 ShouldOptimize 的实现,里面有详细定义何时启动优化:
作为开发视角来看:
  1. 1.
    L371 已经优化过的代码不会再优化
  2. 2.
    L375 这段逻辑决定是否启用 maglev*
  3. 3.
    L386 通过参数主动禁用/或者省电模式等这类不会优化 ( 比如 node --v8-options="--turbo_filter=xxxxx" )
  4. 4.
    L394 运行足够多次才会优化 (还有个配置项 efficiency
    mode
    delay_turbofan 配置延迟多久启动 turbofan)
  5. 5.
    L402 太长的函数不会优化
备注:maglev 是去年 chrome v8 团队搞的新特性:编译层次优化,总的来说就是根据 feekback 对机器码的编译层次做精细控制来达到更好的优化效果,下图是 v8 团队发布的 benchmark 对比:
loading...

# V8 对象模型

本节开始是本文的重点部分,因为只有了解 V8 对象的内存构造,才能真正理解 V8 诸多优化的理由。

C 语言的 struct 是怎么实现「点读」的 ?

在正式进入之前,我们先看看 C 里面 struct 的「点读」是怎么做的:
C 会将 struct 理解为一段连续的线性 buffer 结构
,并在上面根据字段的类型来划分好从下标的哪里到哪里是哪个字段(对齐),因此在编译
point.x
的时候会改成
base+4
的方式进行属性访问,如下图所示,时间复杂度是
O(1)
的:
也因此 C 里面没提供从字段 key 名的方式去取 struct value 的方法,也就是不支持
point['x']
这样,需要你自己写 getter 才能实现类似操作 ——
这类根据 string value 来从对象取值的技术通常在现代编程语言里都是自带了的,通常称为反射,可以在运行时访问源码信息。
但在 JS 里,对象是动态的,可以有任意多的 key-values,而且这些 kv 键值对还可能在运行时期间动态发生变化,比如我可以随时 p.xxx =123 又或者 delete p.xxx 去删掉它,这意味着一个 object 的 “shapes” 及其「内存结构」是无法被静态分析出来的,而且这种内存结构必然不是「定长固定」的,是需要动态 malloc 变长的。
假设现在是 2008 年,你是 google 的工程师,正在 chrome v8 项目组开发,你会怎样设计 JS 的对象的内存结构?
00// obj 的内存结构可以设计成怎样?
01const obj = { x: 3, y: 5 }
一眼丁真,开搞:
一个 key 定义加一个值,然后将这个结构数组化就可以表达对象的 kv 结构,增加属性就在后面继续扩增,查找算法则是从头查到尾,时间复杂度为 O(n), 如下图所示这般:
但是如果按这个设计,下面两个 obj 就会有重复的 key 定义内存消耗了:
00const o1 = { x: 11, y: 22 } // "x" 11 "y" 22
01const o2 = { x: 33, y: 44 } // "x" 33 "y" 44
02                            // 会重复 "x" 和 "y"
好了就上面这样简单弄一下就搞出了好多问题了,从下面开始正式进入,V8 是怎么描述对象的,参见下文

JSObject 与 named-properties & indexed-elements

在 js 标准里 Array 是一类特殊的 Object,但出于性能考虑 V8 底层针对对象和数组的处理是不同的:
  • 所谓 indexed-elements 指的是数组元素(以数字下标作为 key)存储于
    *elements
    ,是一段线性内存空间,可以直接用下标直接访问,查找速度非常快
  • 而其他的普通成员所谓 named-properties 则存储于
    *properties
    查找速度比较慢,需要遍历对比
如下图所示,JSObject:
loading...
在 V8 里:
  1. 1.
    Array-indexed 的属性存储在
    *elements
    里,查找速度快;Named Properties 则存储在
    *properties
    里,查找速度慢
  2. 2.
    Properties / Elements 这两个结构可以是数组,但有时候也会变成字典(比如稀疏数组场景,线性内存空间就不够性能了)
  3. 3.
    每个 JSObject 都有一个
    *hiddenClass
    , 用于保存对象的 Shapes
嗯?对象的 Shapes?那是什么?

对象的 Shapes

所谓对象的 shapes,其实就是对象上有什么 key,前面提到过 V8 的优化需要在运行时不断收集 feedback,比如当执行下面这段代码的时候,引擎就可以知道「obj 有两个 key,一个是 a 一个是 b」:
00const obj = {}
01obj.a = 123;
02obj.b = 124;
03doSomething(obj);
V8 通过 Hidden Class 结构来记录 JSObject 在运行时的时候有哪些 key,也就是记录对象的 shapes,由于 JSObject 是动态的,后续也可以随意设置 obj.xxx = 123,也就是对象的 shapes 会变,也因此对象持有的 Hidden Class 会随着特定代码的运行而变化
Hidden Class 是比较学术的说法,在 V8 源码里的「工程命名」是 Map,在微软 Edge Chakra (edge) 里叫做 Types,在 JavaScriptCore (WebKit Safari) 里叫做 Structure,在 SpiderMonkey (FireFox) 里叫做 Shapes .... 总之各个主流引擎都有实现追踪「对象 shapes 变化」
后文可能会混淆上面几个用语,它们都是指 Hidden Class,用来描述对象的 shapes。

Hidden Class DescriptorArrays 与 in-object properties

前面提到除了
*properties
*elements
可以用来存储对象成员之外,JSObject 还提供了所谓
in-object properties
的方式来存储对象成员,也就是将对象成员保存在 JSObject 自身上,并配合 Hidden Class 进行键值描述:
loading...
上图里 Hidden Class 里底下有个叫做 DescriptorArrays 的子结构,这个结构会记录对象成员 key 以及其对应存储的 in-object 下标,也就是上面的紫框。
读到这里,或许你会问:
  1. 1.
    为什么要这样,这样做能帮助提升性能么?别急,后文会扣回来。
  2. 2.
    什么时候用 in-object 什么时候用 *properties 存储,两者做的是同一件事,不会冲突吗?别急,后文会提。

变化中的 Hidden Class

如果 Hidden Class 是静态的,那么这图就足够描述 Hidden Class 了:
loading...
但是对象的 shapes 会变,也因此对象持有的 Hidden Class 会随着特定代码的运行而变化,V8 使用了 Transition Chain,一种基于链表构造的方式来描述「变化中的 Hidden Class」:
loading...
备注:为了方便讨论,后文可能不会将 Hidden Class 画成链表,而是画成一起并且省略空对象的 shapes,另外 Hidden Class Node 上还有其他字段,相对不那么重要,就忽略了
由于链表的特性,显然可以比较容易地让具有相同 shapes 的对象能复用同一个 Hidden Class ,比如下面这个 case,o1 o2 均复用了地址为
0xABCD
的 Hidden Class 节点:
loading...
当出现不同走向的时候,此时会单独开一个 branch 来描述这种情况,此时 o1 和 o2 就不再一样了:
loading...

V8 对象模型总结

从前文的讨论,可以得到的结论:
  1. 1.
    V8 使用 JSObject 来描述对象,上面有若干个字段(除了上面那些还有 prototype 原型链那些,相对不那么重要,就没画出)
  2. 2.
    V8 还使用 Tagged Pointer 来描述对象指针(前文有提)
  3. 3.
    named properties 成员存储在 *properties 里,可以为数组,也可以为字典
  4. 4.
    named properties 也可以存储在 in-object properties 里,可以动态增长。
  5. 5.
    数字下标成员存储在 *elements 里,可以为数组,也可以为字典(稀疏数组场景)
悬而未决的问题:
  1. 1.
    何时用 in-object properties 何时用 *properties ?
  2. 2.
    为什么看起来 Hidden Class 这套机制下属性查找依然是 O(n) 的操作?追踪对象的 shapes 意义在哪?
请带着这两个问题到下一章 Inline Caches 继续阅读。

# V8 Inline Caches (ICs) 优化原理

引入 Hidden Class 后,为了读取某个成员,那不还得查一次 Hidden Class 拿到 in-object 的下标,这个过程不还是 O(n) 吗?
是的,如果事先不知道 JSObject 的 shapes 的情况下去读取成员确实是 O(n) 的,但前面我已经提过了:
V8 的诸多优化是基于 assumption 的,那么在已知 obj 的 Shapes 的情况下,你会怎么优化下面这个 distance 函数?
loading...
如此优化就将通过 key 访问成员的 O(n) 过程优化为 O(1) 按下标偏移直接读取了,这种优化手段就叫做 Inline Caches (ICs),有点像 C 语言的 struct 将字段点读编译为偏移访问,只不过这个编译是 JIT 的,不是 C 那样 AOT 静态编译确定的,是 V8 在函数执行多次收集了足够多的 feedback 后实现的。
你可能还会问:在调用优化后的 distance2 的时候具体要怎么确定传入的 p1 p2 的 shapes 是否有变化?还记得前面那个 0xABCD 吗?没错,编译后的汇编 checkpoint 就是直接判断传入对象的 hidden classs 指针数值是不是 0xABCD,如果不是就触发「反优化」兜底解释器模式运行即可。
—— 怎么?感觉有点迷惑? 下面这个实例将手把手展开 ICs 的真实场景以及汇编细节

汇编实例:为什么静态的比动态的要好 ?

从前面 Inline Cache 的讨论中可以得知,必须要确定了访问的 key 才能做 ICs 优化,因此写代码的过程中,如有可能请尽量避免下面这样通过 key string 动态查找对象属性:
00function test(obj: any, key: string) {
01  return obj[key]; 
02}
如果能明确知道 key 的具体值,此时建议写为:
00function test(obj: any, key: 'a' | 'b') {
01  if (key === 'a') return obj.a;
02  if (key === 'b') return obj.b;
03}
即使确实不得不动态查询,但是你知道某个子 case 占了 99% 的调用次数,此时也可以这样优化:
00function test(obj, key: 'a' | 'b') {
01  // 为 'a' 的调用次数占了 99% 可以这样提前优化
02  if (key === 'a') return obj.a;
03  return obj[key];
04}
静态和动态两种写法风格可能会有几倍甚至上百倍的差距,如果业务里有大几百万次的调用 test,优化后能省不少毫秒,比如下面这个「简化的服务发现」例子有近百倍的差距:
原因是 s2.js 里那些属性访问都被 ICs 技术优化成 O(1) 访问了,速度很快 —— 为了探究内部的 ICs 相关汇编逻辑,尝试输出 serviecMap 的 Hidden Class (V8 里 hidden class 别名是 Map) 以及汇编源码:
首先
%DebugPrint
serviceMap
的 Hidden Class 的物理地址,可以看到是
0x3a8d76b74971
然后看后续编译优化的 arm machine code 是怎么利用这个地址实现 ICs 技术优化的:(我这会的电脑是 mac m1 因此是 arm 汇编,不是 x86 汇编)
可以看到,ICs 的 checkpoint 其实就是将 Hidden Map 的指针物理地址 inline cached 到汇编里了,如果 check 通过那么就可以基于这个假设直接将属性访问优化为 O(1) 的 in-object properties 访问了,这也是这个技术为什么叫做 Inline Cahce (ICs) 了
(这几乎是 V8 里效果最好的优化了,也因此部分 benchmark 里 nodejs 对象可能比 Java 对象还快,因为 Java 里有可能滥用反射导致对象性能非常差)

Fast Properties 和 Slow Properties

如果知道 ICs 技术内涵的话,理解 Fast Properties 和 Slow Properties (或者称字典模式) 就不会有困难了。
右图描述了 JSObject 的主要构造:当把对象成员存储到 in-object properties 的时候,此时称对象是 Fast Properties 模式,这意味着对象访问 V8 会在合适的时候将其 Inline Cache 到优化后的汇编里;
反之,当成员存储到
*properties
的时候,此时称为 Slow Properties,此时就不会对这类对象做 inline cache 优化了,此时对象访问性能最差(因为要遍历
*properties
字典[1]20250317 update: 这里有个错误,*properties 字段也可以是数组的、这种情况也能算是 Fast Properties,感谢@Doctor-wu的指正,通常慢几十到几百倍,取决于对象成员数量
我们可以用
%HasFastProperties
来打印对象是否是 Fast Properties 模式,如下图所示
loading...
delete 会将对象转为 slow properties 模式,为什么呢?因为 delete 带来的问题可太多了,缓存技术最怕的就是 delete,如图所示:
loading...
我拍脑子就能想到上面四个问题,要完整的确保 delete 的安全性可太难了,因此维护 delete 后的 hidden class 非常麻烦,V8 采取的方式是直接将 in-object 释放掉,然后将对象属性都复制存储到
*properties
里了,以后这个对象就不再开启 ICs 优化了,此时这种退化后的对象就称为 slow properties (或者称字典模式)

# EOF

下面这些内容比较散,我简单列一下:
  • 把 V8 ICs 这套搞懂让我爽了快一个月 ...
  • 愈发鄙视动态写法了,比如我曾经喷过用 array.includes 来做这种语法糖,这种写法会让 ICs 失效的,性能我简单测了一下至少慢了五十倍:
    00if (key === 'aa' ||
    01  key === 'bb' ||
    02  key === 'cc') { }
    03// => 改写为: 
    04if (['aa', 'bb', 'cc'].includes(key)) { }
  • 字面量申请的空对象 V8 会额外预先分配一些空的 in-object,然后通过
    Slack Tracking 松弛追踪
    技术在合适的时候释放没有用到的空间 —— 为什么要预先分配?因为 V8 假设空对象后面都会增加属性上去
    00const o1 = {};  // o1 shallow size 为 28 
    01const o2 = { ggg: undefined }  // o2 为 16
    02const o3 = Object.create(null) // o3 为 12
  • 输出一个暴论:依据前文提到的 ICs 优化,原型链继承将会大大影响 prototype 上函数的 JIT 效率,尤其是子类有很多自己的属性的时候,而从前面的 add 函数可知,这种 case 下性能可能会慢好几倍 🐢
  • Safari 也有 JIT 也有 ICs 技术, 根据 ICs 优化原理写的代码很多程度上全平台通用 (其实主流 JS 引擎都实现了 ICs, 包括 FireFox)
  • 其他代码建议看我授权发表在公司官号上的内容,那里详细一点 (这篇文章首发于公司内网): 极速优化:十倍提升JS代码运行效率的技巧
最后的最后,总之参考了大量资料,感谢互联网以及 GPT, 让我得以站在巨人的肩膀上研究 V8 及其 JIT 优化细节,不敢说 100% 精通,但现在如果让我从头写一个 js jit 编译器我大概是能知道要怎么写了:




回到顶部