Sparkplug的“前辈”V8使用比较快的Full-codegen编译器生成未优化的代码,然后使用即时(JIT)编译器Crankshaft进行优化并编译代码。反优化。Full-codegen+Crankshaft性能问题,不适合JS优化。V8为移动优先管道引入了Ignition解释器和TurboFan的优化编译器。最后通过Ignition生成字节码,再通过TurboFan生成优化后的代码,并进行相关的反优化。与Crankshaft相比,TurboFan不足。V8打造了一个集Full-codegen、Crankshaft、Ignition和TurboFan于一体的“全家桶”流水线。Sparkplug的目的是快速编译。首先,它编译的函数已经编译成字节码。第二个是Sparkplug不像TurboFan那样生成任何中间代码(IR)。//Sparkplug编译器的部分代码for(;!iterator.done();iterator.Advance()){VisitSingleBytecode();}堆栈指针和帧指针的使用每当我们调用一个新函数时,它都会创建一个函数局部变量的新栈帧。堆栈帧由帧指针(标记其开始)和堆栈指针(标记其结束)定义。Sparkplug使用“解释器兼容的堆栈框架”。当函数创建一个新的栈帧时,它还会将旧的帧指针保存在栈上,并将新的帧指针设置为它自己的栈帧的开头。除了函数的局部变量和回调地址外,还会有参数的传递和栈的存储。参数(包括接收者)在函数调用前按倒序入栈,帧指针前面的栈槽分别是当前调用的函数、上下文和传递的参数个数。这是“标准”的JS框架布局:Ignition解释器进一步使调用约定更加明确。Ignition是一个基于寄存器的解释器,它与机器寄存器的不同之处在于它是一个虚拟寄存器。它的作用是存储解释器的当前状态。包括JavaScript函数局部变量(var/let/const声明)和临时值。堆栈帧中还有一个指向正在执行的字节码数组的指针,以及当前字节码在该数组中的偏移量。后来,V8团队对解释器栈帧做了一个小改动,即Sparkplug在代码执行过程中不再保留最新的字节码偏移量,而是存储从Sparkplug代码到对应字节码偏移量的地址范围。双向映射。Sparkplug有意创建并维护一个与解释器匹配的栈帧布局;每当解释器存储一个寄存器值时,Sparkplug也会存储一个。这样做有几个优点。一是它简化了Sparkplug的编译。Sparkplug只能镜像解释器的行为,而不必保留从解释器寄存器到Sparkplug状态的映射。二是它还加快了编译速度,因为字节码编译器已经完成了寄存器分配的繁琐工作。三是其与系统其余部分(如调试器、分析器)的集成基本适配。第四,任何适用于解释器的栈上替换(OSR,on-stackreplacement)的逻辑都适用于Sparkplug;解释器和Sparkplug代码之间的堆栈帧转换成本几乎为零。尽管Sparkplug的工作与解释器几乎相同。它只是由解释器执行的序列化,调用相同的内置函数并维护相同的堆栈帧。Sparkplug也很有价值,因为它消除了,或者更严格地说,预编译,那些无法消除的解释器成本,因为在实践中,解释器会影响许多CPU优化。比如运算符解码和下一个字节码调度。解释器从内存中动态读取静态运算符,导致CPU停止或推测值可能是什么;调度到下一个字节码需要成功的分支预测来保持性能,即使推测和预测是正确的,执行所有解码和调度代码仍然会耗尽各种缓冲区和缓存中的宝贵空间。虽然CPU用于机器码,但实际上它本身就是一个解释器。从这个角度来看,Sparkplug其实是一个从Ignition到CPU字节码的“转换器”,将功能从“模拟器”转移到“本机”运行。极客时间《Jvascript进阶实战课》学习笔记Day14
