day13 通过SparkPlug深入了解调用栈

day13 通过SparkPlug深入了解调用栈_第1张图片
Sparkplug的“前身”
V8 用的是一个相对比较快的 Full-codegen 编译器生成未优化的代码,然后通过 Crankshaft 这个及时(JIT)编译器对代码进行优化和反优化。
Full-codegen + Crankshaft 性能问题,对JS优化不理想。
day13 通过SparkPlug深入了解调用栈_第2张图片
V8 就引入了 Ignition 解释器和 TurboFan 的优化编译器以移动优先为目的的流水线。通过 Ignition 最终生成字节码,之后再通过 TurboFan 生成优化后的代码,和做相关的反优化。
TurboFan 和 Crankshaft 比起来,显得不足。
day13 通过SparkPlug深入了解调用栈_第3张图片
V8 又创建了一个把 Full-codegen,Crankshaft,Ignition 和 TurboFan 整合起来的“全家桶”的流水线。
day13 通过SparkPlug深入了解调用栈_第4张图片
Sparkplug 的目的是快速编译
首先,它编译的函数已经被编译为字节码了。第二个方法是 Sparkplug 不会像 TurboFan 那样生成任何中间码 (IR)。
day13 通过SparkPlug深入了解调用栈_第5张图片

//  Sparkplug 编译器的部分代码
for (; !iterator.done(); iterator.Advance()) {
  VisitSingleBytecode();
}

栈指针和帧指针的使用
调用栈是代码执行存储函数状态的方式,而每当我们调用一个新函数时,它都会为该函数的本地变量创建一个新的栈帧。 栈帧由帧指针(标记其开始)和栈指针(标记其结束)定义。Sparkplug采用了与“与解释器兼容的栈帧”。
当该函数创建一个新栈帧时,也会将旧的帧指针保存在栈中,并将新的帧指针设置为它自己栈帧的开头。
day13 通过SparkPlug深入了解调用栈_第6张图片
除了函数的本地变量和回调地址外,栈中还会有传参和储值。参数(包括接收者)在调用函数之前以相反的顺序压入栈内,帧指针前面的几个栈槽是当前正在被调用的函数、上下文,以及传递的参数数量。这是“标准” JS 框架布局:
day13 通过SparkPlug深入了解调用栈_第7张图片
Ignition 解释器会进一步让调用约定变得更加明确。Ignition 是基于寄存器的解释器,和机器寄存器的不同在于它是一个虚拟寄存器。它的作用是存储解释器的当前状态。包括 JavaScript 函数局部变量(var/let/const 声明)和临时值。
栈帧中还有一个指向正在执行的字节码数组的指针,以及当前字节码在该数组中的偏移量。

后来,V8 团队对解释器栈帧做了一个小改动,就是 Sparkplug 在代码执行期间不再保留最新的字节码偏移量,改为了存储从 Sparkplug 代码地址范围到相应字节码偏移量的双向映射。
day13 通过SparkPlug深入了解调用栈_第8张图片
Sparkplug 特意创建并维护与解释器相匹配的栈帧布局;每当解释器存储一个寄存器值时,Sparkplug 也会存储一个。
它这样做有几点好处,一是简化了 Sparkplug 的编译, Sparkplug 可以只镜像解释器的行为,而不必保留从解释器寄存器到 Sparkplug 状态的映射。二是它还加快了编译速度,因为字节码编译器已经完成了寄存器分配的繁琐的工作。三是它与系统其余部分(如调试器、分析器)的集成是基本适配的。四是任何适用于解释器的栈替换(OSR,on-stack replacement)的逻辑都适用于 Sparkplug;并且解释器和 Sparkplug 代码之间交换的栈帧转换成本几乎为零。
Sparkplug虽然和解释器做几乎同样的工作。只是解释器执行的序列化,调用相同的内置功能并维护相同的栈帧。
Sparkplug也是有价值的,因为它消除了或更严谨地说,预编译了那些无法消除的解释器成本,因为实际上,解释器影响了许多 CPU 优化。

例如操作符解码和下一个字节码调度。解释器从内存中动态读取静态操作符,导致 CPU 要么停止,要么推测值可能是什么;分派到下一个字节码需要成功的分支预测才能保持性能,即使推测和预测是正确的,仍然必须执行所有解码和分派代码,结果依然是用尽了各种缓冲区中的宝贵空间和缓存。尽管 CPU 用于机器码,但本身实际上就是一个解释器。从这个角度看,Sparkplug 其实是一个从 Ignition 到 CPU 字节码的“转换器”,将函数从“模拟器”中转移到“本机”运行。

你可能感兴趣的:(前端javascript)