导语:JavaScript以简单易用而著称,NodeJS的出现使JavaScript的影响进一步扩大。JavaScript是动态类型的语言,动态类型为应用开发者带来了便利,但也为JavaScript运行时的性能带来了负担,例如类型的不断变化可能会导致基于类型的某些优化失效。为了解决JavaScript由于动态类型导致的运行性能受损问题,各大JavaScript引擎几乎都采用了IC(Inline Cache)技术:即通过缓存上一次对象的类型信息来加速当前对象属性的读写访问。本文从引例入手,以V8 JavaScript引擎(主要由于V8既是Chrome浏览器的JS引擎,也是node的JS引擎)为基础,深入分析Inline Cache机制的基本原理。(编辑:中间件小Q妹)
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 |
3. 类型反馈向量(type feedback vector)
前面已经提到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> |
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和Megamorphic
function 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态的性能:
综合前文所述,仅从Inline cache的角度来分析,如果JavaScript开发者在应用开发时能让IC态保持在monomorphic或者polymorphic,代码的性能是最好的。特别是对于一些比较注重应用冷启动性能的场景,减少启动过程中的IC-Miss会使启动时间大幅缩短。
【参考文献】
作者介绍
廖彬, 腾讯云中间件高级语言虚拟机核心研发,曾是阿里巴巴车联网系统AliOS JavaScript虚拟机核心研发人员,有多年高级语言虚拟机开发经验,熟悉各类GC算法、JavaScript Runtime等领域。