本文轉載自:衆成翻譯
譯者:LexHuang
連接:http://www.zcfy.cc/article/2959
原文:http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.htmljavascript
我知道如何實現用語言(或者語言的子集)來實現運行該語言虛擬機。若是我在學校或者有更多的時間我確定會用JavaScript實現一個JavaScript虛擬機。實際上這並不會變成一個獨一無二的JavaScript項目,由於蒙特利爾大學的人所造的Tachyon已經在某種程度上達到了一樣的目的,可是我也有些我本身想要追求的點子。php
我則有另外一個和自循環虛擬機緊密相關的夢想。我想要幫助JavaScript開發者理解JS引擎的工做方式。我認爲理解你正在使用的工具是咱們職業生涯中最重要的。越多人不在把JS VM看做是將JavaScript源碼轉爲0-1神祕的黑盒越好。html
我應該說我不是一我的在追求如何解釋虛擬機的內部機制而且幫助人們編寫性能更好的代碼。全世界有許多人正在嘗試作一樣的事情。可是我認爲又一個問題正在阻止知識有效地被開發者所吸取——咱們正在用錯誤的形式來傳授咱們的知識。我對此深感愧疚:java
有時候我把我對V8的瞭解包裝成了很難消化的「作這個,別作那個」的教條化意見。這樣的問題在於它對於解釋起不到任何幫助而且很容易隨着關注人的減小而消失。git
有時候咱們用了錯誤的抽象層次來解釋虛擬機的工做機制。我喜歡一個想法:看見盡是彙編代碼的ppt演示可能會鼓勵人們去學習彙編而且學會以後會去讀ppt演示的內容。但我也懼怕有時候這些ppt只會被人忽視和遺忘而對於實踐毫無用處。github
我一直在思考這些問題很長時間了而且我認爲用JavaScript來解釋JavaScript虛擬機是一個值得嘗試的事情。我在WebRebels 2012發表的演講「V8 Inside Out」追求的正是這一點[視頻][演示]而且在本文中我像回顧我一直在奧斯陸所談論的事情可是不一樣的是不會有任何音頻的干擾。(我認爲我寫做的方式比我演講的方式更加嚴肅些 ☺)。web
想象你想要爲了一個在語法上很是相似於JavaScript可是有着更簡單的對象模型的語言——用表來映射key到任意類型的值來代替JavaScript對象——而來用JavaScript實現其虛擬機。簡單起見,讓咱們想象Lua, 既像JavaScript但做爲一個語言又很不同。我最喜歡的「造出一個充滿點的數組而後去計算向量合」的例子看起來大體以下:shell
function MakePoint(x, y) local point = {} point.x = x point.y = y return point end function MakeArrayOfPoints(N) local array = {} local m = -1 for i = 0, N do m = m * -1 array[i] = MakePoint(m * i, m * -i) end array.n = N return array end function SumArrayOfPoints(array) local sum = MakePoint(0, 0) for i = 0, array.n do sum.x = sum.x + array[i].x sum.y = sum.y + array[i].y end return sum end function CheckResult(sum) local x = sum.x local y = sum.y if x ~= 50000 or y ~= -50000 then error("failed: x = " .. x .. ", y = " .. y) end end local N = 100000 local array = MakeArrayOfPoints(N) local start_ms = os.clock() * 1000; for i = 0, 5 do local sum = SumArrayOfPoints(array) CheckResult(sum) end local end_ms = os.clock() * 1000; print(end_ms - start_ms)
注意我有一個至少檢查某些最終結果的微型基準測試的習慣。這有助於當有人發現個人革命性的jsperf測試用例只不過是我本身的bug時,讓我不會太尷尬。express
若是你拿上面的例子放入一個Lua編譯器你會獲得相似於下面的東西:vim
∮ lua points.lua 150.2
很好,可是對於瞭解虛擬機的工做過程起不到任何幫助。因此讓咱們想一想若是咱們有用JavaScript編寫的類Lua虛擬機會長什麼樣。「類」是由於我不想實現徹底相似於Lua的語法,我更喜歡只關注於用表來實現對象這一點上。原生編譯器應該會將咱們的代碼編譯成下面的JavaScript:
function MakePoint(x, y) { var point = new Table(); STORE(point, 'x', x); STORE(point, 'y', y); return point; } function MakeArrayOfPoints(N) { var array = new Table(); var m = -1; for (var i = 0; i <= N; i++) { m = m * -1; STORE(array, i, MakePoint(m * i, m * -i)); } STORE(array, 'n', N); return array; } function SumArrayOfPoints(array) { var sum = MakePoint(0, 0); for (var i = 0; i <= LOAD(array, 'n'); i++) { STORE(sum, 'x', LOAD(sum, 'x') + LOAD(LOAD(array, i), 'x')); STORE(sum, 'y', LOAD(sum, 'y') + LOAD(LOAD(array, i), 'y')); } return sum; } function CheckResult(sum) { var x = LOAD(sum, 'x'); var y = LOAD(sum, 'y'); if (x !== 50000 || y !== -50000) { throw new Error("failed: x = " + x + ", y = " + y); } } var N = 100000; var array = MakeArrayOfPoints(N); var start = LOAD(os, 'clock')() * 1000; for (var i = 0; i <= 5; i++) { var sum = SumArrayOfPoints(array); CheckResult(sum); } var end = LOAD(os, 'clock')() * 1000; print(end - start);
可是若是你嘗試用d8(V8的獨立shell)去運行編譯後的代碼,它會很禮貌的拒絕:
∮ d8 points.js points.js:9: ReferenceError: Table is not defined var array = new Table(); ^ ReferenceError: Table is not defined at MakeArrayOfPoints (points.js:9:19) at points.js:37:13
失敗的緣由很簡單:咱們還缺乏負責實現對象模型和存取語法的運行時系統代碼。這可能看起來很明顯,可是我想要強調的是:虛擬機從外面看起來像是黑盒,在內部其實是一系列盒子爲了獲得出最佳性能的相互協做。這些盒子是:編譯器、運行時例程、對象模型、垃圾回收等。幸運的是咱們的語言和例子很是簡單因此咱們的運行時系統僅僅多了幾行代碼:
function Table() { // Map from ES Harmony is a simple dictionary-style collection. this.map = new Map; } Table.prototype = { load: function (key) { return this.map.get(key); }, store: function (key, value) { this.map.set(key, value); } }; function CHECK_TABLE(t) { if (!(t instanceof Table)) { throw new Error("table expected"); } } function LOAD(t, k) { CHECK_TABLE(t); return t.load(k); } function STORE(t, k, v) { CHECK_TABLE(t); t.store(k, v); } var os = new Table(); STORE(os, 'clock', function () { return Date.now() / 1000; });
注意到我用了ES6的Map而不是通常的JavaScript對象由於潛在的表可使用任何鍵,而不只是字符串形式的。
∮ d8 **--harmony** quasi-lua-runtime.js points.js 737
如今咱們編譯後的代碼能夠執行可是卻慢地使人失望,由於每一次讀和寫不得不跨越全部這些抽象層級後才能拿到值。讓咱們經過全部JavaScript虛擬機都有的最基本的優化inline caching來嘗試減小這些開銷。即便是用Java實現的JS虛擬機最終也會使用它由於動態調用的本質是被暴露在字節碼層面的結構化的內聯緩存。Inline caching(在V8資源裏一般簡寫爲IC)其實是一門近30年的很是古老的技術,最初用在Smalltalk虛擬機上。
內聯緩存(Inline caching)背後的思想很是簡單:建立一個高速路來繞過運行時系統來讀取對象的屬性:對傳入的對象及其屬性做出某種假設,而後經過一個低成本的方式驗證這個假設是否正確,若是正確就讀取上次緩存的結果。在充滿了動態類型和晚綁定以及其餘古怪行爲——好比eval——的語言裏對一個對象做出合理的假設是很是困難的,因此咱們退而求其次,讓咱們的讀/寫操做可以有學習能力:一旦它們看見某個對象它們就能夠以某種方式來自適應,使得以後的讀取操做在遇到相似結構的對象時可以更快地進行。在某種意義上,咱們將要在讀/寫操做上緩存關於以前見過的對象的佈局的相關知識——這也是內聯緩存這個名字的由來。內聯緩存能夠被用在幾乎全部須要動態行爲的操做上,只要你能夠找到正確的高速路:算數操做、調用自由函數、方法調用等等。有些內聯緩存還能緩存不止一條快速通道,這些內聯緩存就變成了多態的。
若是咱們開始思考如何應用內聯緩存到上面編譯後的代碼,答案就變得顯而易見了:咱們須要改變咱們的對象模型。咱們不可能從一個map
中進行快速讀取,由於咱們老是要調用get
方法。[若是咱們可以窺探map
後的純哈希表,咱們就能夠經過緩存桶索引來讓內聯緩存替咱們工做而不須要相處一個新的對象佈局。]
出於效率角度考慮,用做數據結構的表應該更相似於C結構:帶有固定偏移量的命名字段序列。這樣表就和數組相似:咱們但願數字形式的屬性的存儲相似於數組。可是很顯然並非全部表的鍵都是數字:鍵能夠被設計成非字符串非數字或者包含太多字符串命名的屬性,而且隨着表的修改鍵也會隨之修改。不幸的是,咱們不能作任何昂貴的類型推斷。取而代之咱們必須找在程序運行期間的每個表背後的結構,而且隨着程序的運行能夠建立和修改它們。幸運的是,有一個衆所周知的技術 ☺:_隱藏類(hidden classes)_。
隱藏類背後的思想能夠歸結爲如下兩點:
對於每一個javascript對象,運行時系統都會將其合一個hidden class關聯起來。就像Java VM會關聯一個java.lang.Class
的實例給每一個對象同樣。
若是對象的佈局改變了,則運行時就會 找到一個hidden class或者建立一個新的hidden class來匹配這個新對象佈局而且鏈接到該對象上。
隱藏類有個很是重要的特性:它們運行虛擬機經過簡單比對緩存過的隱藏類來檢查關於某個對象佈局的假設。這正是咱們的內聯緩存功能所須要的。讓咱們爲咱們的類-Lua運行時來實現一些簡單的隱藏類系統。每一個隱藏類本質上是屬性描述符的集合,每一個描述符要麼是一個真正的屬性,要麼是一個過渡(transition):從一個沒有該屬性的類指向一個有該屬性的類。
function Transition(klass) { this.klass = klass; } function Property(index) { this.index = index; } function Klass(kind) { // Classes are "fast" if they are C-struct like and "slow" is they are Map-like. this.kind = kind; this.descriptors = new Map; this.keys = []; }
過渡之因此存在是爲了讓多個對象之間能共享隱藏類:若是你有兩個對象共享了隱藏類而且你爲它們同時增長了某些屬性,你不但願獲得不一樣的隱藏類。
Klass.prototype = { // Create hidden class with a new property that does not exist on // the current hidden class. addProperty: function (key) { var klass = this.clone(); klass.append(key); // Connect hidden classes with transition to enable sharing: // this == add property key ==> klass this.descriptors.set(key, new Transition(klass)); return klass; }, hasProperty: function (key) { return this.descriptors.has(key); }, getDescriptor: function (key) { return this.descriptors.get(key); }, getIndex: function (key) { return this.getDescriptor(key).index; }, // Create clone of this hidden class that has same properties // at same offsets (but does not have any transitions). clone: function () { var klass = new Klass(this.kind); klass.keys = this.keys.slice(0); for (var i = 0; i < this.keys.length; i++) { var key = this.keys[i]; klass.descriptors.set(key, this.descriptors.get(key)); } return klass; }, // Add real property to descriptors. append: function (key) { this.keys.push(key); this.descriptors.set(key, new Property(this.keys.length - 1)); } };
如今咱們可讓咱們的表變得更加靈活而且能容許它們自適應其自身地構造方式
var ROOT_KLASS = new Klass("fast"); function Table() { // All tables start from the fast empty root hidden class and form // a single tree. In V8 hidden classes actually form a forest - // there are multiple root classes, e.g. one for each constructor. // This is partially due to the fact that hidden classes in V8 // encapsulate constructor specific information, e.g. prototype // poiinter is actually stored in the hidden class and not in the // object itself so classes with different prototypes must have // different hidden classes even if they have the same structure. // However having multiple root classes also allows to evolve these // trees separately capturing class specific evolution independently. this.klass = ROOT_KLASS; this.properties = []; // Array of named properties: 'x','y',... this.elements = []; // Array of indexed properties: 0, 1, ... // We will actually cheat a little bit and allow any int32 to go here, // we will also allow V8 to select appropriate representation for // the array's backing store. There are too many details to cover in // a single blog post :-) } Table.prototype = { load: function (key) { if (this.klass.kind === "slow") { // Slow class => properties are represented as Map. return this.properties.get(key); } // This is fast table with indexed and named properties only. if (typeof key === "number" && (key | 0) === key) { // Indexed property. return this.elements[key]; } else if (typeof key === "string") { // Named property. var idx = this.findPropertyForRead(key); return (idx >= 0) ? this.properties[idx] : void 0; } // There can be only string&number keys on fast table. return void 0; }, store: function (key, value) { if (this.klass.kind === "slow") { // Slow class => properties are represented as Map. this.properties.set(key, value); return; } // This is fast table with indexed and named properties only. if (typeof key === "number" && (key | 0) === key) { // Indexed property. this.elements[key] = value; return; } else if (typeof key === "string") { // Named property. var index = this.findPropertyForWrite(key); if (index >= 0) { this.properties[index] = value; return; } } this.convertToSlow(); this.store(key, value); }, // Find property or add one if possible, returns property index // or -1 if we have too many properties and should switch to slow. findPropertyForWrite: function (key) { if (!this.klass.hasProperty(key)) { // Try adding property if it does not exist. // To many properties! Achtung! Fast case kaput. if (this.klass.keys.length > 20) return -1; // Switch class to the one that has this property. this.klass = this.klass.addProperty(key); return this.klass.getIndex(key); } var desc = this.klass.getDescriptor(key); if (desc instanceof Transition) { // Property does not exist yet but we have a transition to the class that has it. this.klass = desc.klass; return this.klass.getIndex(key); } // Get index of existing property. return desc.index; }, // Find property index if property exists, return -1 otherwise. findPropertyForRead: function (key) { if (!this.klass.hasProperty(key)) return -1; var desc = this.klass.getDescriptor(key); if (!(desc instanceof Property)) return -1; // Here we are not interested in transitions. return desc.index; }, // Copy all properties into the Map and switch to slow class. convertToSlow: function () { var map = new Map; for (var i = 0; i < this.klass.keys.length; i++) { var key = this.klass.keys[i]; var val = this.properties[i]; map.set(key, val); } Object.keys(this.elements).forEach(function (key) { var val = this.elements[key]; map.set(key | 0, val); // Funky JS, force key back to int32. }, this); this.properties = map; this.elements = null; this.klass = new Klass("slow"); } };
[我不打算一行一行地解釋上面的代碼,由於它已是用JavaScript書寫的了;而不是C++ 或者 彙編...這正是使用JavaScript的意義所在。然而你能夠經過評論或者郵件來詢問任何不理解的地方。]
既然咱們已經在運行時系統里加入了隱藏類,使得咱們可以快速檢查對象的結構而且經過它們的索引來快速讀取屬性,咱們只差實現內聯緩存了。這須要在編譯器和運行時系統增長一些新的功能(還記得我談論過虛擬機內不一樣成員之間的協做麼?)。
實現內聯緩存的途徑之一是將其分割成兩個部分:生成代碼裏的可變調用點和能夠被調用點調用的一系列存根(stubs,一小片生成的本地代碼)。很是重要的一點是:存根自己必須能從調用它們的調用點(或者運行時系統)中找到:存根只存放特定假設下的編譯後的快速路徑,若是這些假設對存根遇到的對象不適用,則存根能夠初始化調用該存根的調用點的變更(打包,patching),使得該調用點可以適應新的狀況。咱們的純JavaScript仍然包含兩個部分:
一個全局變量,每一個ic都會使用一個全局變量來模擬可變調用指令;
並使用閉包來代替存根。
在本地代碼裏, V8經過在棧上監聽返回地址來找到要打包的內聯緩存點。咱們不能經過純JavaScript來達到這一點(arguments.caller
的粒度不夠細)。因此咱們將只會顯式地傳遞內聯緩存的id到內聯緩存的存根。經過內聯緩存優化後的代碼以下:
// Initially all ICs are in uninitialized state. // They are not hitting the cache and always missing into runtime system. var STORE$0 = NAMED_STORE_MISS; var STORE$1 = NAMED_STORE_MISS; var KEYED_STORE$2 = KEYED_STORE_MISS; var STORE$3 = NAMED_STORE_MISS; var LOAD$4 = NAMED_LOAD_MISS; var STORE$5 = NAMED_STORE_MISS; var LOAD$6 = NAMED_LOAD_MISS; var LOAD$7 = NAMED_LOAD_MISS; var KEYED_LOAD$8 = KEYED_LOAD_MISS; var STORE$9 = NAMED_STORE_MISS; var LOAD$10 = NAMED_LOAD_MISS; var LOAD$11 = NAMED_LOAD_MISS; var KEYED_LOAD$12 = KEYED_LOAD_MISS; var LOAD$13 = NAMED_LOAD_MISS; var LOAD$14 = NAMED_LOAD_MISS; function MakePoint(x, y) { var point = new Table(); STORE$0(point, 'x', x, 0); // The last number is IC's id: STORE$0 ⇒ id is 0 STORE$1(point, 'y', y, 1); return point; } function MakeArrayOfPoints(N) { var array = new Table(); var m = -1; for (var i = 0; i <= N; i++) { m = m * -1; // Now we are also distinguishing between expressions x[p] and x.p. // The fist one is called keyed load/store and the second one is called // named load/store. // The main difference is that named load/stores use a fixed known // constant string key and thus can be specialized for a fixed property // offset. KEYED_STORE$2(array, i, MakePoint(m * i, m * -i), 2); } STORE$3(array, 'n', N, 3); return array; } function SumArrayOfPoints(array) { var sum = MakePoint(0, 0); for (var i = 0; i <= LOAD$4(array, 'n', 4); i++) { STORE$5(sum, 'x', LOAD$6(sum, 'x', 6) + LOAD$7(KEYED_LOAD$8(array, i, 8), 'x', 7), 5); STORE$9(sum, 'y', LOAD$10(sum, 'y', 10) + LOAD$11(KEYED_LOAD$12(array, i, 12), 'y', 11), 9); } return sum; } function CheckResults(sum) { var x = LOAD$13(sum, 'x', 13); var y = LOAD$14(sum, 'y', 14); if (x !== 50000 || y !== -50000) throw new Error("failed x: " + x + ", y:" + y); }
上述的改變依舊是不言自明的:每個屬性的讀/寫點都有屬於它們本身的、帶有id的內聯緩存。距離最終完成還剩一小步:實現未命中(MISS)
存根和能夠生存特定存根的「存根編譯器」:
function NAMED_LOAD_MISS(t, k, ic) { var v = LOAD(t, k); if (t.klass.kind === "fast") { // Create a load stub that is specialized for a fixed class and key k and // loads property from a fixed offset. var stub = CompileNamedLoadFastProperty(t.klass, k); PatchIC("LOAD", ic, stub); } return v; } function NAMED_STORE_MISS(t, k, v, ic) { var klass_before = t.klass; STORE(t, k, v); var klass_after = t.klass; if (klass_before.kind === "fast" && klass_after.kind === "fast") { // Create a store stub that is specialized for a fixed transition between classes // and a fixed key k that stores property into a fixed offset and replaces // object's hidden class if necessary. var stub = CompileNamedStoreFastProperty(klass_before, klass_after, k); PatchIC("STORE", ic, stub); } } function KEYED_LOAD_MISS(t, k, ic) { var v = LOAD(t, k); if (t.klass.kind === "fast" && (typeof k === 'number' && (k | 0) === k)) { // Create a stub for the fast load from the elements array. // Does not actually depend on the class but could if we had more complicated // storage system. var stub = CompileKeyedLoadFastElement(); PatchIC("KEYED_LOAD", ic, stub); } return v; } function KEYED_STORE_MISS(t, k, v, ic) { STORE(t, k, v); if (t.klass.kind === "fast" && (typeof k === 'number' && (k | 0) === k)) { // Create a stub for the fast store into the elements array. // Does not actually depend on the class but could if we had more complicated // storage system. var stub = CompileKeyedStoreFastElement(); PatchIC("KEYED_STORE", ic, stub); } } function PatchIC(kind, id, stub) { this[kind + "$" + id] = stub; // non-strict JS funkiness: this is global object. } function CompileNamedLoadFastProperty(klass, key) { // Key is known to be constant (named load). Specialize index. var index = klass.getIndex(key); function KeyedLoadFastProperty(t, k, ic) { if (t.klass !== klass) { // Expected klass does not match. Can't use cached index. // Fall through to the runtime system. return NAMED_LOAD_MISS(t, k, ic); } return t.properties[index]; // Veni. Vidi. Vici. } return KeyedLoadFastProperty; } function CompileNamedStoreFastProperty(klass_before, klass_after, key) { // Key is known to be constant (named load). Specialize index. var index = klass_after.getIndex(key); if (klass_before !== klass_after) { // Transition happens during the store. // Compile stub that updates hidden class. return function (t, k, v, ic) { if (t.klass !== klass_before) { // Expected klass does not match. Can't use cached index. // Fall through to the runtime system. return NAMED_STORE_MISS(t, k, v, ic); } t.properties[index] = v; // Fast store. t.klass = klass_after; // T-t-t-transition! } } else { // Write to an existing property. No transition. return function (t, k, v, ic) { if (t.klass !== klass_before) { // Expected klass does not match. Can't use cached index. // Fall through to the runtime system. return NAMED_STORE_MISS(t, k, v, ic); } t.properties[index] = v; // Fast store. } } } function CompileKeyedLoadFastElement() { function KeyedLoadFastElement(t, k, ic) { if (t.klass.kind !== "fast" || !(typeof k === 'number' && (k | 0) === k)) { // If table is slow or key is not a number we can't use fast-path. // Fall through to the runtime system, it can handle everything. return KEYED_LOAD_MISS(t, k, ic); } return t.elements[k]; } return KeyedLoadFastElement; } function CompileKeyedStoreFastElement() { function KeyedStoreFastElement(t, k, v, ic) { if (t.klass.kind !== "fast" || !(typeof k === 'number' && (k | 0) === k)) { // If table is slow or key is not a number we can't use fast-path. // Fall through to the runtime system, it can handle everything. return KEYED_STORE_MISS(t, k, v, ic); } t.elements[k] = v; } return KeyedStoreFastElement; }
代碼很長(以及註釋),可是配合上面全部解釋應該不難理解:內聯緩存負責觀察而存根編譯器/工程負責生產自適應和特化後的存根[有心的讀者可能注意到了我本能夠初始化全部鍵控的存儲內聯緩存(keyed store ICs),用一開始的快速讀取或者當它進入快速狀態後就一直保持住]。
若是咱們無論上面全部代碼而回到咱們的「基準測試」,咱們會獲得很是使人滿意的結果:
∮ d8 --harmony quasi-lua-runtime-ic.js points-ic.js 117
這要比咱們一開始的天真嘗試提高了6倍!
但願你在閱讀這一部分的時候已經看完了以前全部內容...我嘗試從不一樣的角度,JavaScript開發者的角度,來看某些驅動當今JavaScript引擎的點子。所寫的代碼越長,我越有一種盲人摸象的感受。下面的事實只是爲了給你一種望向深淵的感受:V8有10種描述符,5種元素類型(+9外部元素類型),ic.cc裏包含了幾乎全部內聯緩存狀態選擇的邏輯多達2500行,而且V8的內聯緩存的狀態不止2個(它們是uninitialized, premonomorphic, monomorphic, polymorphic, generic states,更別提用於鍵控讀/寫的內聯緩存的特殊的狀態或者是算數內斂緩存的徹底不一樣的狀態層級),ia32-specific手寫的內聯緩存存根多達5000行代碼,等等。這些數字只會隨着時間的流逝和V8爲了識別和適應愈來愈多的對象佈局的學習而增加。並且我甚至都還沒談到對象模型自己(objects.cc 13k行代碼),或者垃圾回收,或者優化編譯器。
話雖如此,在可預見的將來內,我確信基礎將不會改變,若是變了確定會引起一場你必定會注意到的巨大的爆炸!所以我認爲此次嘗試用JavaScript去理解基礎的練習是很是很是很是重要的。
我但願明天或者幾周以後你會停下來而且大喊「我找到了!」而且告訴你的爲何特定狀況下在一個地方爲一個對象增長屬性會影響其他很遠的接觸這些對象的熱迴路的性能。_你知道的,由於隱藏類變了!_