如何分析JS引擎中的Inline Cache技术
如何分析JS引擎中的Inline Cache技术,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
01 引例function Point(x,y) {
this.x = x;
this.y = y;
}
var p = new Point(0, 1);
var q = new Point(2,3);
var r = new Point(4,5);
为了避免API调用不稳定因素的影响,通过修改V8源码,在内部插入时间戳的方式。在3.2G 8核机器上,分别测试三次调用new Point(x,y)时执行this.x=x这个语句耗时,结果如下表所示。
执行代码 | this.x=x耗时统计 |
var p = new Point(0,1); | 4.11ns |
var q = new Point(2,3); | 6.63ns |
var r = new Point(4,5); | 0.65ns |
从表中的结果可以看出,事实并非想象中的从第二次执行开始,速度就会变快,相反,第二次比第一次还要慢,到第三次的时候速度才会变快。本文后面会通过分析V8 IC机制来解释为什么第二次速度最慢,第三次执行速度又变快的原因。
02 问题分析1. 对象的隐藏类(Hidden Class)
由于JavaScript对象没有类型信息,几乎所有JS引擎都采用隐藏类(Hidden Class/Shape/Map等)来描述对象的布局信息,用以在虚拟机内部区分不同对象的类型,从而完成一些基于类型的优化。
V8对JavaScript对象都使用HeapObject来描述和存储,每一种JavaScript对象都是HeapObject的子类,而每个HeapObject都用Map来描述对象的布局。对象的Map描述了对象的类型,即成员数目、成员名称、成员在内存中的位置信息等。
上述源码中对象p、q、r都是由同一个构造函数Point生成,因此他们具有同样的内存布局,可以采用同一个Map来描述。
2. 隐藏类变迁(Map Transition)
因为JavaScript是高度动态的程序设计语言,对象的成员可以被随意动态地添加、删除甚至修改类型。因此,对象的隐藏类在程序的运行过程中可能会发生变化,V8内部把这种变化叫隐藏类变迁(Map Transition)。
Map Transition示意图
前面已经提到IC机制的原理是:对于某代码语句比如this.x=x,比较上次执行到该语句时缓存的Map和对象当前的Map是否相同,如果相同则执行对应的IC-Hit代码,反之执行IC-Miss代码。那么V8是如何组织被缓存的Map和IC-Hit代码?以上文代码为例,V8会在Point函数对象上添加一个名为type_feedback_vector的数组成员,对于该函数中的每处可能产生IC的代码,Point对象中的type_feedback_vector会缓存上一次执行至该语句时对象的Map和对应的IC-Hit代码(在V8内部称为IC-Hit Handler)。上文中的Point函数中有两处可能产生IC的语句,this.x=x和this.y=y。假设某次执行至this.x=x时,对象this的Map是map0,执行至this.y=y时this的Map是map1,那么Point对象的type_feedback_vector数据内容如下所示:
数组下标 | IC对应的源码 | 缓存的Map和对应的IC-Hit Handler |
0 | this.x=x | <map0, ic-hit handler> |
1 | this.y=y | <map1, ic-hit handler> |
简单来说,type_feedback_vector缓存了Map和与之对应的IC-Hit handler,这样IC相关的逻辑简化为只需要通过访问type_eedback_vector就可以判断是否IC Hit并执行对应的IC-Hit Handler。
4. IC状态机为了描述V8中IC状态的变化情况,本节将以状态机的形式描述V8中最常见IC种类的状态变化情况。V8中最常用 的IC分为五个状态,如图二所示。初始为uninitialized状态,当发生一次IC-Miss时会变为pre-monomorphic态,再次IC-Miss会进入monomorphic态,如果继续IC-Miss,则会进入polymorphic状态。进入polymorphic之后如果继续IC-Miss 3次,则会进入megamorphic态,并最终稳定在megamophic态。
IC状态机
引例中代码会涉及到IC状态机的前三种状态。
以Point函数走红this.x=x语句为例,第一次执行时,由于Point.type_feedback_vetor为空,因此此时会发生IC-Miss,并将该处IC状态从uninitialized设置为pre-monomorphic,IC-Miss Handler会分析出此时this对象的Map中不包含属性x,因此会添加成员x,接着会发生Map Transition,即前文提到的this对象的隐藏类从map0变为map1。由于考虑到大部分函数可能只会被调用一次,因此V8的策略是发生第一次IC-Miss时,并不会缓存此时的map,也不会产生IC-Hit handler;
第二次调用构造函数执行this.x=x时,由于Point.type_feedback_vector仍然为空,因此会发生第二次IC-Miss,并将IC状态修改为monomorphic,此次IC-Miss Hanlder除了发生Map Transition之外,还会编译生成IC-Hit Handler,并将map0和IC Hit Handler缓存到Point.type_feedback_vector中。由于此次IC-Miss Handler需要编译IC-Hit Handler的操作比较耗时,因此第二次执行this.x=x是最慢的;
第三次调用构造函数中this.x=x时,发现Point.type_feedback_vector不为空,且此时缓存的map0与此时this对象的Map也是一致的,因此会直接调用IC-Hit Handler来添加成员x并进行Map transition。由于此次无需对map0进行分析,也无需编译IC-Hit Handler,因此此时执行效率比前两次都高。
至此,已经解释清楚为什么V8执行构造函数时,第二遍最慢而第三遍最快的原因。5. Polymorphic和Megamorphicfunction f(o) {
return o.x;
}
f({x:1}) //pre-monomorphic
f({x:2}) //monomorphic
f({x:3, y:1}) // polymorphic degree 2
f({x:4, z:1}) // polymorphic degree 3
f({x:5, a:1}) // polymorphic degree 4
f({x:6, b:1}) // megamorphic
上述代码描述了图二状态机中polymorphic态和megamophic态的两种情形。上面3中提到type_feedback_vector会缓存Map和IC-Hit Handler,但是如果IC状态太多比如到达megamorphic态,此时Map和IC-Hit Handler便不会再缓存在Point对象的feedback_vector中,而是存储在固定大小的全局hashtable中,如果IC态多于hashtable的大小,则会对之前的缓存进行覆盖。通过上述分析,可以总结得出不同IC态的性能:- 如果每次都能在monomorphic态IC-Hit,代码的运行速度是最快的;
- 在polymorphic态IC-Hit时,需要对缓存进行线性查找;
- Megamorphic是性能最低的IC-Hit,因为需要每次对hashtable进行查找,但是megamorphic ic hit性能仍然优 于IC-Miss;
IC-Miss性能是最差的;
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注蜗牛博客行业资讯频道,感谢您对蜗牛博客的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
评论