JS 引擎中的 Inline Cache 技术内幕,你知道多少?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript以简单易用而著称,NodeJS的出现使JavaScript的影响进一步扩大。JavaScript是动态类型的语言,动态类型为应用开发者带来了便利,但也为JavaScript运行时的性能带来了负担,例如类型的不断变化可能会导致基于类型的某些优化失效。为了解决JavaScript由于动态类型导致的运行性能受损问题,各大JavaScript引擎几乎都采用了IC(Inline Cache)技术:即通过缓存上一次对象的类型信息来加速当前对象属性的读写访问。本文从引例入手,以V8 JavaScript引擎(主要由于V8既是Chrome浏览器的JS引擎,也是node的JS引擎)为基础,深入分析Inline Cache机制的基本原理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"引例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function Point(x,y) {\nthis.x = x;\nthis.y = y;\n}\nvar p = new Point(0, 1);\nvar q = new Point(2,3);\nvar r = new Point(4,5);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了避免API调用不稳定因素的影响,通过修改V8源码,在内部插入时间戳的方式。在3.2G 8核机器上,分别测试三次调用new Point(x,y)时执行this.x=x这个语句耗时,结果如下表所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
执行代码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"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从表中的结果可以看出,事实并非想象中的从第二次执行开始,速度就会变快,相反,第二次比第一次还要慢,到第三次的时候速度才会变快。本文后面会通过分析V8 IC机制来解释为什么第二次速度最慢,第三次执行速度又变快的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"问题分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 对象的隐藏类(Hidden Class)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于JavaScript对象没有类型信息,几乎所有JS引擎都采用隐藏类(Hidden Class\/Shape\/Map等)来描述对象的布局信息,用以在虚拟机内部区分不同对象的类型,从而完成一些基于类型的优化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"V8对JavaScript对象都使用HeapObject来描述和存储,每一种JavaScript对象都是HeapObject的子类,而每个HeapObject都用Map来描述对象的布局。对象的Map描述了对象的类型,即成员数目、成员名称、成员在内存中的位置信息等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述源码中对象p、q、r都是由同一个构造函数Point生成,因此他们具有同样的内存布局,可以采用同一个Map来描述。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 隐藏类变迁(Map Transition)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为JavaScript是高度动态的程序设计语言,对象的成员可以被随意动态地添加、删除甚至修改类型。因此,对象的隐藏类在程序的运行过程中可能会发生变化,V8内部把这种变化叫隐藏类变迁(Map Transition)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上文代码为例,当Point function被声明时,V8就会给Point创建隐藏类map0,由于暂时还没有属性,因此map0为空。当执行this.x=x时,V8会创建第二个Hidden Class map1,map1是基于map0,并且描述了属性x在内存中的位置,此时this对象的Hidden Class会通过Map Transition变为map1。当执行this.y=y时,会重复前面的操作,一个新的Hidden Class map2会被创建,此时this对象的Hidden Class被更新为map2。this对象的Map从map0变为map1,再变成map2的过程就叫做Map Transition。图一描述了Point类对象创建过程中Map Transition的过程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/22\/6a\/22f7296f70bc70842c3f85a53443726a.jpg","alt":null,"title":"Map Transition示意图","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 类型反馈向量(type feedback vector)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面已经提到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数据内容如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
数组下标IC对应的源码缓存的Map和对应的IC-Hit Handler
0this.x=x<map0, ic-hit handler>
1this.y=y<map1, ic-hit handler>"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"简单来说,type_feedback_vector缓存了Map和与之对应的IC-Hit handler,这样IC相关的逻辑简化为只需要通过访问type_eedback_vector就可以判断是否IC Hit并执行对应的IC-Hit Handler。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4. IC状态机"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了描述V8中IC状态的变化情况,本节将以状态机的形式描述V8中最常见IC种类的状态变化情况。V8中最常用 的IC分为五个状态,如图二所示。初始为uninitialized状态,当发生一次IC-Miss时会变为pre-monomorphic态,再次IC-Miss会进入monomorphic态,如果继续IC-Miss,则会进入polymorphic状态。进入polymorphic之后如果继续IC-Miss 3次,则会进入megamorphic态,并最终稳定在megamophic态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/db\/01\/dbeca0a80f171062fde41f3d60f55401.png","alt":null,"title":"IC状态机","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引例中代码会涉及到IC状态机的前三种状态。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以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;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二次调用构造函数执行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是最慢的;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三次调用构造函数中this.x=x时,发现Point.type_feedback_vector不为空,且此时缓存的map0与此时this对象的Map也是一致的,因此会直接调用IC-Hit Handler来添加成员x并进行Map transition。由于此次无需对map0进行分析,也无需编译IC-Hit Handler,因此此时执行效率比前两次都高。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,本节已经解释清楚为什么V8执行构造函数时,第二遍最慢而第三遍最快的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5. Polymorphic和Megamorphic"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function f(o) {\n return o.x;\n}\nf({x:1}) \/\/pre-monomorphic\nf({x:2}) \/\/monomorphic\nf({x:3, y:1}) \/\/ polymorphic degree 2\nf({x:4, z:1}) \/\/ polymorphic degree 3\nf({x:5, a:1}) \/\/ polymorphic degree 4\nf({x:6, b:1}) \/\/ megamorphic"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代码描述了图二状态机中polymorphic态和megamophic态的两种情形。上面3中提到type_feedback_vector会缓存Map和IC-Hit Handler,但是如果IC状态太多比如到达megamorphic态,此时Map和IC-Hit Handler便不会再缓存在Point对象的feedback_vector中,而是存储在固定大小的全局hashtable中,如果IC态多于hashtable的大小,则会对之前的缓存进行覆盖。通过上述分析,可以总结得出不同IC态的性能:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果每次都能在monomorphic态IC-Hit,代码的运行速度是最快的;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在polymorphic态IC-Hit时,需要对缓存进行线性查找;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Megamorphic是性能最低的IC-Hit,因为需要每次对hashtable进行查找,但是megamorphic ic hit性能仍然优于IC-Miss;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"IC-Miss性能是最差的;"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"综合前文所述,仅从Inline cache的角度来分析,如果JavaScript开发者在应用开发时能让IC态保持在monomorphic或者polymorphic,代码的性能是最好的。特别是对于一些比较注重应用冷启动性能的场景,减少启动过程中的IC-Miss会使启动时间大幅缩短。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"参考文献"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/v8.dev\/docs","title":"","type":null},"content":[{"type":"text","text":"https:\/\/v8.dev\/docs"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/mathiasbynens.be\/notes\/shapes-ics","title":"","type":null},"content":[{"type":"text","text":"https:\/\/mathiasbynens.be\/notes\/shapes-ics"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/richardartoul.github.io\/jekyll\/update\/2015\/04\/26\/hidden-classes.html","title":"","type":null},"content":[{"type":"text","text":"https:\/\/richardartoul.github.io\/jekyll\/update\/2015\/04\/26\/hidden-classes.html"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/mrale.ph\/blog\/2012\/06\/03\/explaining-js-vms-in-js-inline-caches.html","title":"","type":null},"content":[{"type":"text","text":"https:\/\/mrale.ph\/blog\/2012\/06\/03\/explaining-js-vms-in-js-inline-caches.html"}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/mrale.ph\/blog\/2015\/01\/11\/whats-up-with-monomorphism.html","title":"","type":null},"content":[{"type":"text","text":"https:\/\/mrale.ph\/blog\/2015\/01\/11\/whats-up-with-monomorphism.html"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"头图"},{"type":"text","text":":Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者"},{"type":"text","text":":廖彬"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/mbJJAiGz0OAd2IOc8K5Mkg","title":"","type":null},"content":[{"type":"text","text":"JS引擎中的Inline Cache技术内幕,你知道多少?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"来源"},{"type":"text","text":":腾讯云中间件 - 微信公众号 [ID:gh_6ea1bc2dd5fd]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"转载"},{"type":"text","text":":著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章