1、字节码

早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。

随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:

  • 时间问题:编译时间过久,影响代码启动速度;
  • 空间问题:缓存编译后的二进制代码占用更多的内存。 这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:
  • 解决启动问题:生成字节码的时间很短;
  • 解决空间问题:字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码还是小了太多,缓存字节码会大大降低内存的使用。
  • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。 如何查看字节码
// test.js
function add(x, y) {
var z = x + y;
return z;
}
console.log(add(1, 2));

运行./d8 ./test.js –print-bytecode:

[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
0x10008250026 @ 0 : 25 02 Ldar a1 #将 a1 寄存器中的值加载到累加器中,LoaD Accumulator from Register
0x10008250028 @ 2 : 34 03 00 Add a0, [0]
0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到 r0 寄存器中
0x1000825002d @ 7 : aa Return #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3

常用字节码指令:

  • Ldar:表示将寄存器中的值加载到累加器中,你可以把它理解为 LoaD Accumulator from Register,就是把某个寄存器中的值,加载到累加器中。
  • Star:表示 Store Accumulator Register, 你可以把它理解为 Store Accumulator to Register,就是把累加器中的值保存到某个寄存器中
  • Add:Add a0, [0] 是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。
  • add a0 后面的 [0] 称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
  • LdaSmi:将小整数(Smi)加载到累加器寄存器中
  • Return:结束当前函数的执行,并将控制权传回给调用方。返回的值是累加器中的值。

image-20220318114529340

V8 中的字节码指令集 | 理解 V8 的字节码「译」

2、机器码(Bytecode)

某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。

如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。