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":":著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章