V8是怎么通过内联缓存来提升函数执行效率的?

参考一段代码,思考如何提高loadX函数的执行效率?

function loadX(o) {
return o.x
}
var o = { x: 1,y:3}
var o1 = { x: 3 ,y:6}
for (var i = 0; i < 90000; i++) {
loadX(o)
loadX(o1)
}

我们定义了一个loadX函数,它有一个参数o,该函数只是返回了o.x。
通常V8获取o.x的流程是这样的:查找对象o的隐藏类,再通过隐藏类查找x属性偏移量,然后根据偏移量获取属性值,在这段代码中loadX函数会被反复执行,那么获取o.x流程也需要反复被执行。
V8采用压缩这个过程的策略就是内联缓存(Inline Cache),简称为IC。

什么是内联缓存?

就是在V8执行函数的过程中,会观察函数中一些调用点(CallSite)上的关键的中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此V8利用IC,可以有效提升一些重复代码的执行效率。
深入分析这一过程,
IC会为每个函数维护一个反馈向量(FeedBack Vector),反馈向量记录了函数在执行过程中的一些关键的中间数据。关于函数和反馈向量的关系你可以参看下图:

反馈向量其实就是一个表结构,它由很多项组成的,每一项称为一个插槽(Slot),V8会依次将执行loadX函数的中间数据写入到反馈向量的插槽中。
loadX的代码如下所示:

function loadX(o) {
o.y = 4
return o.x
}

当V8执行这段函数的时候,它会判断 o.y = 4和 return o.x这两段是调用点(CallSite),因为它们使用了对象和属性,那么V8会在loadX函数的反馈向量中为每个调用点分配一个插槽。
每个插槽中包括了插槽的索引(slot index)、插槽的类型(type)、插槽的状态(state)、隐藏类(map)的地址、还有属性的偏移量。如果函数的调用点都使用了对象o,那么反馈向量两个插槽中的map属性也都是指向同一个隐藏类的,因此两个插槽的map地址也是一样的。

loadX函数中的关键数据是如何被写入到反馈向量中?

function loadX(o) {
return o.x
}
loadX({x:1})

将loadX转换为字节码:

StackCheck
LdaNamedProperty a0, [0], [0]
Return

这段代码的字节码含义:
第一句是检查栈是否溢出;
第二句是LdaNamedProperty,它的作用是取出参数a0的第一个属性值,并将属性值放到累加器中;
第三句是返回累加器中的属性值。
关键关注LdaNamedProperty这句字节码,它有三个参数:
a0就是loadX第一个参数
第二个参数[0]表示取出对象a0的第一个属性值
第三个参数就和反馈向量相关了,它表示将LdaNamedProperty操作的中间数据写入到反馈向量中,方括号中间的0表示写入反馈向量的第一个插槽中。

在map栏,缓存了o的隐藏类的地址;
在offset一栏,缓存了属性x的偏移量;
在type一栏,缓存了操作类型,这里是LOAD类型。在反馈向量中,我们把这种通过o.x来访问对象属性值的操作称为LOAD类型。
V8除了缓存o.x这种LOAD类型的操作以外,存储(STORE)类型和函数调用(CALL)类型的中间数据。
代码:

function foo(){}
function loadX(o) {
o.y = 4
foo()
return o.x
}
loadX({x:1,y:4})

字节码:

StackCheck
LdaSmi [4]
StaNamedProperty a0, [0], [0]
LdaGlobal [1], [2]
Star r0
CallUndefinedReceiver0 r0, [4]
LdaNamedProperty a0, [2], [6]
Return

执行流程:

这段代码是先使用LdaSmi [4],将常数4加载到累加器中,然后通过StaNamedProperty的字节码指令,将累加器中的4赋给o.y,这是一个存储(STORE)类型的操作。

调用foo函数的字节码是:

LdaGlobal [1], [2]
Star r0
CallUndefinedReceiver0 r0, [4]

CallUndefinedReceiver0,来实现foo函数的调用,并将执行的中间结果放到反馈向量的第5个插槽中,这是一个调用(CALL)类型的操作。

多态和超态

一个反馈向量的一个插槽中可以包含多个隐藏类的信息,那么:
如果一个插槽中只包含1个隐藏类,那么我们称这种状态为单态(monomorphic);
如果一个插槽中包含了2~4个隐藏类,那我们称这种状态为多态(polymorphic);
如果一个插槽中超过4个隐藏类,那我们称这种状态为超态(magamorphic)。如果函数的反馈向量中存在多态或者是超多态的情况,效率肯定是要低于单态的。

尽量保持单态

总的来说,我们只需要记住一条就足够了,那就是单态的性能优于多态和超态,所以我们需要稍微避免多态和超态的情况。
要避免多态和超态,那么就尽量默认所有的对象属性是不变的,比如你写了一个loadX(o)的函数,那么当传递参数时,尽量不要使用多个不同形状的o对象。

此文章为5月Day22学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪

豪猪
4 声望4 粉丝

undefined