[譯文] JavaScript工作原理:V8引擎內(nèi)部+5條優(yōu)化代碼的竅門

原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code

幾周前我們開始了一個系列博文旨在深入挖掘 JavaScript 并弄清楚它的工作原理:我們認(rèn)為通過了解 JavaScript 的構(gòu)建單元并熟悉它們是怎樣結(jié)合起來的,有助于寫出更好的代碼和應(yīng)用盏浙。

這個系列的第一篇文章聚焦于提供一個關(guān)于引擎政供、運(yùn)行時和調(diào)用棧的概述椒振。本文將會深入分析 GoogleV8 引擎的內(nèi)部實現(xiàn)。我們也會提供一些編寫更優(yōu)質(zhì) JavaScript 代碼的小技巧——我們的團(tuán)隊在構(gòu)建 SessionStack 應(yīng)用時遵循的最佳實踐澎迎。

概述

JavaScript 引擎是執(zhí)行 JavaScript 代碼的程序或解釋器。 JavaScript 引擎可以實現(xiàn)為標(biāo)準(zhǔn)的解釋器夹供,或即時編譯器仁堪,以某種形式將 JavaScript 編譯成字節(jié)碼。

以下是一些流行的 JavaScript 引擎項目:

  • V8 —— 開源弦聂,Google 開發(fā),C++ 編寫
  • Rhino ?—— Mozilla 基金會管理莺葫,開源匪凉,完全使用 Java 開發(fā)
  • SpiderMonkey —— 第一個 JavaScript 引擎,以前由 Netscape Navigator 維護(hù)徙融,現(xiàn)在由 Firefox 維護(hù)
  • JavaScriptCore —— 開源洒缀,以 Nitro 的名義銷售瑰谜,由 Apple 公司為 Safari 瀏覽器開發(fā)
  • KJS? —— KDE 的引擎欺冀,最初由 Harri PortenKDE 項目的 Konqueror 瀏覽器開發(fā)
  • Chakra (JScript9)? —— IE 瀏覽器
  • Chakra (JavaScript)? —— Edge 瀏覽器
  • Nashorn —— OpenJDK 開源項目的一部分,由 Oracle Java 和其工具集開發(fā)
  • JerryScript? —— 一個輕量級的物聯(lián)網(wǎng)引擎

為什么要創(chuàng)建V8引擎萨脑?

谷歌公司研發(fā)的 V8 引擎是由 C++ 編寫的開源引擎隐轩。該引擎使用在谷歌瀏覽器內(nèi)部。但與其他引擎不同的是渤早,V8 也應(yīng)用于 Node.js 這一流行的運(yùn)行時當(dāng)中职车。

V8 最初是為了提高瀏覽器中 JavaScript 執(zhí)行的性能而設(shè)計的。為了獲得速度鹊杖,V8JavaScript 代碼轉(zhuǎn)換成更高效的機(jī)器編碼而不是使用解釋器悴灵。同其他現(xiàn)代 JavaScript 引擎如 SpiderMonkeyRhinoMozilla)所做的一樣,V8 通過實現(xiàn)即時編譯器在執(zhí)行時將 JavaScript 代碼編譯成機(jī)器代碼骂蓖。其中最主要的區(qū)別是 V8 不生成字節(jié)碼或任何中間代碼积瞒。

V8曾有兩個編譯器

V8 5.9版本發(fā)布之前(2017年初發(fā)布),該引擎使用兩個編譯器:

  • full-codegen —— 簡單登下、非趁?祝快的編譯器叮喳,生成簡單和相對較慢的機(jī)器代碼
  • Crankshaft ?—— 更加復(fù)雜的(即時)優(yōu)化編譯器,生成高度優(yōu)化的代碼

同時 V8 內(nèi)部使用了多條線程:

  • 主線程的工作正如你所預(yù)期:獲取代碼缰贝、編譯然后執(zhí)行代碼
  • 另有一條獨(dú)立線程負(fù)責(zé)編譯馍悟,這樣主線程可以在前者優(yōu)化代碼時繼續(xù)執(zhí)行
  • 一條分析器線程會告訴運(yùn)行時,哪些方法會耗費(fèi)大量時間以便 Crankshaft 編譯器優(yōu)化代碼
  • 還有幾條線程處理垃圾回收清理

首次執(zhí)行 JavaScript 代碼時剩晴,V8 利用 full-codegen 無過渡地直接將解析后的 JavaScript 轉(zhuǎn)換成機(jī)器代碼锣咒。這使得它可以非常快速地開始執(zhí)行機(jī)器代碼赞弥。注意 V8 不使用中間代碼表示宠哄,因此擺脫了對解釋器的需要。

在你的代碼運(yùn)行了一定時間后毛嫉,分析線程就能收集到足夠的數(shù)據(jù)判斷哪些方法需要優(yōu)化承粤。

接著闯团,Crankshaft 優(yōu)化在另一線程開始房交。它將 JavaScript 抽象語法樹轉(zhuǎn)換成高級靜態(tài)單賦值(SSA)表示,稱為 Hydrogen(注:氮)刃唤,并嘗試優(yōu)化氮圖尚胞。大多數(shù)優(yōu)化都在這個級別完成帜慢。

內(nèi)聯(lián)

優(yōu)化的第一步是先內(nèi)聯(lián)盡可能多的代碼粱玲。內(nèi)聯(lián)是一個將調(diào)用引用(函數(shù)調(diào)用的那行代碼)替換成所調(diào)用的函數(shù)體的過程。這個簡單的步驟使接下來的優(yōu)化過程更有意義:

隱藏類

JavaScript 是基于原型的語言:沒有允青,使用克隆的方式創(chuàng)建對象昧廷。JavaScript 還是一個動態(tài)編程語言,這意味著當(dāng)對象被初始化之后還可以輕易地增刪其屬性皆串。

大多數(shù) JavaScript 解釋器采用類字典數(shù)據(jù)結(jié)構(gòu)(基于哈希函數(shù))來存儲對象屬性值在內(nèi)存中的位置眉枕。這種結(jié)構(gòu)使得在 JavaScript 中取回屬性值的計算開銷比非動態(tài)語言如 JavaC#更昂貴速挑。在 Java 中,所有的對象屬性在編譯前就由固定對象布局決定了翅萤,不允許在運(yùn)行時動態(tài)增加或刪除(C#有動態(tài)類型套么,但那是另一個話題)胚泌。因此肃弟,屬性值(或指向?qū)傩缘闹羔槪┚涂梢砸赃B續(xù)緩沖區(qū)存儲在內(nèi)存中笤受,之間用固定的偏移量隔開。偏移量的長度簡單地根據(jù)屬性的類型確定绅项,然而這在 JavaScript 中是不可能的,因為屬性類型可以在運(yùn)行時更改囊陡。

由于通過字典查找對象屬性在內(nèi)存中的位置非常低效撞反,V8 采用了另一方法作為替代:隱藏類。隱藏類的原理類似于 Java 等語言中使用的固定對象布局(類)嘹害,除了是在運(yùn)行時創(chuàng)建。現(xiàn)在幢踏,讓我們來看看它們實際是什么樣的:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);

當(dāng) new Point(1, 2) 調(diào)用發(fā)生房蝉,V8 將創(chuàng)建了一個名為 C0 的隱藏類。

現(xiàn)在 Point 還沒有定義任何屬性微渠,所以 C0 是空的。

一旦第一條聲明 this.x = x 開始執(zhí)行(在 Point 函數(shù)內(nèi))檀蹋,V8 將創(chuàng)建第二個基于 C0 的隱藏類 C1续扔。C1 描述了在內(nèi)存中(相對于 point 對象)能找到屬性 x 的位置焕数。在這個例子中堡赔,x 保存在偏移量為 0 的位置,這意味著在將內(nèi)存中的對象視作一個連續(xù)緩沖區(qū)時灼捂,第一個偏移量對應(yīng)著 x悉稠。V8 還會通過一個“類轉(zhuǎn)換”更新 C0,以表明如果一個屬性 x 被添加到 point 對象中的猛,隱藏類 C0 就會轉(zhuǎn)換成 C1想虎。下面 point 對象的隱藏類現(xiàn)在變成了 C1舌厨。

每次添加一個新屬性到對象,舊隱藏類都會通過一個轉(zhuǎn)換路徑更新成一個新隱藏類署浩。隱藏類轉(zhuǎn)換之所以如此重要是因為它能使隱藏類在以同樣方式創(chuàng)建的對象間共享筋栋。如果兩個對象共享同一個隱藏類并向它們添加相同的屬性器联,轉(zhuǎn)換可以確保它們獲得相同的隱藏類和所有與其相關(guān)的優(yōu)化代碼。

當(dāng) this.y = y 語句執(zhí)行時將會重復(fù)同樣的過程(同樣在 Point 函數(shù)內(nèi)肴颊,this.x = x 之后)渣磷。

新的隱藏類 C2 將被創(chuàng)建醋界,C1 發(fā)生類轉(zhuǎn)換表示如果向一個 Point 對象添加屬性 y (已經(jīng)包含一個屬性 x),隱藏類應(yīng)該更新為 C2丘侠,并且 point 對象的隱藏類更新為 C2蜗字。

隱藏類轉(zhuǎn)換依賴向?qū)ο笏砑訉傩缘捻樞蚺膊丁U埧聪旅娴拇a片段:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;

現(xiàn)在你可能會假設(shè) p1p2 使用相同的隱藏類和轉(zhuǎn)換级零。實際則并非如此奏纪。對于 p1,先添加屬性 a 然后添加屬性 b亥贸。而對于 p2,先添加的屬性是 b 然后才是 a男韧。因此,由于轉(zhuǎn)換路徑不同此虑, p1p2 最終將會產(chǎn)生不同的隱藏類朦前。在這種情況下韭寸,最好在初始化動態(tài)屬性時保持順序一致以便復(fù)用相同的隱藏類恩伺。

內(nèi)聯(lián)緩存

V8 利用了另一項叫做內(nèi)聯(lián)緩存的技術(shù)來優(yōu)化動態(tài)類型語言。內(nèi)聯(lián)緩存依賴于這樣一種觀察:同一方法的重復(fù)調(diào)用通常發(fā)生在同一類型的對象上凰荚。關(guān)于內(nèi)聯(lián)緩存的深入闡述在這里便瑟。

我們準(zhǔn)備介紹內(nèi)聯(lián)緩存的一般概念(以免你沒有時間查看上述的深入闡述)番川。

那么它的原理是什么爽彤?V8 維護(hù)著在最近的方法調(diào)用中作為參數(shù)傳入的對象類型的緩存适篙,并利用這個信息假設(shè)未來會被當(dāng)做參數(shù)的對象的類型嚷节。如果 V8 能很好地假設(shè)出將要傳入方法的對象的類型,就能直接越過如何獲取對象屬性的計算過程衩婚,取而代之的是使用之前查找對象的隱藏類時存儲的信息非春。

那么隱藏類是如何與內(nèi)聯(lián)緩存關(guān)聯(lián)起來的?每當(dāng)某一對象調(diào)用方法時护侮,V8 必須執(zhí)行對此對象的隱藏類的查詢來確定訪問某個屬性的偏移量羊初。當(dāng)對同一隱藏類成功調(diào)用過兩次同樣的方法后什湘,V8 將省略對隱藏類的查詢而只將屬性偏移量添加到對象指針本身长赞。對于那個方法未來所有的調(diào)用得哆,V8 都假定隱藏類不改變柳恐,并利用之前查詢存儲的偏移量直接跳到某一屬性的內(nèi)存地址乐设。這極大地提高了執(zhí)行速度近尚。

內(nèi)聯(lián)緩存也是同類對象共享同一隱藏類如此重要的原因。如果你創(chuàng)建了擁有不同隱藏類的兩個同類對象(正如前面的例子)也搓,V8 就無法使用內(nèi)聯(lián)緩存,因為即便這兩個對象是相同的類型拒迅,但他們對應(yīng)的隱藏類為屬性指定了不同的偏移量她倘。

這兩個對象基本相同前硫,但 `a`屹电、`b` 屬性的創(chuàng)建順序不同。

編譯到機(jī)器代碼

一旦氮圖優(yōu)化好后个扰,Crankshaft 會將它降為更低水平的表示葱色,稱為 Lithium(注:鋰)苍狰。大多數(shù) Lithium 的實現(xiàn)依賴于特定架構(gòu)淋昭。寄存器分配發(fā)生在這個級別翔忽。

最終歇式,Lithium 被編譯成機(jī)器代碼材失。隨后發(fā)生 OSR:堆棧上替換。在開始編譯和優(yōu)化明顯長時間運(yùn)行的方法前硫豆,我們可能會運(yùn)行它熊响。V8 不會在再次開始執(zhí)行優(yōu)化版本時忘記那些緩慢的執(zhí)行。而是轉(zhuǎn)換我們所有的上下文(棧秸弛,寄存器)以便能在執(zhí)行中切換到優(yōu)化版本剔难。這是個非常復(fù)雜的任務(wù)胆屿,記住在其他的優(yōu)化中,V8 最先做了代碼內(nèi)聯(lián)偶宫。V8 不是唯一有這種能力的引擎非迹。

還有種被稱為反優(yōu)化的安全措施能做反向轉(zhuǎn)換,回退到未優(yōu)化代碼纯趋,以防引擎做出的假設(shè)不再成立憎兽。

垃圾回收

在垃圾回收方面冷离,V8 采用傳統(tǒng)分代方法標(biāo)記和清掃來清理老的代。標(biāo)記階段會暫停 JavaScript 的執(zhí)行纯命。為了控制垃圾回收的開銷并使執(zhí)行更加穩(wěn)定西剥,V8 采用增量標(biāo)記:它不遍歷全部棧堆,而是嘗試標(biāo)記每一個可能的對象咆畏,它只遍歷棧堆的一部分麦牺,然后恢復(fù)正常執(zhí)行魏颓。下一次垃圾回收暫停會在之前棧堆的停止位置繼續(xù)枕荞。這可使正常執(zhí)行期間只發(fā)生相當(dāng)短的暫停渣刷。正如之前提到的瞭吃,清理階段由單獨(dú)的線程處理。

Ignition 和 TurboFan

隨著2017年初 V8 5.9版本的發(fā)布止状,一個新的執(zhí)行管道被引入。新的管道在實際的JavaScript 應(yīng)用中實現(xiàn)了更大的性能提升和的顯著的內(nèi)存節(jié)省伏社。

新的執(zhí)行管道構(gòu)建在 V8 的解釋器 IgnitionV8 最新的優(yōu)化編譯器 TurboFan 之上。

你可以在這里查閱 V8 團(tuán)隊關(guān)于這個主題的博文妨马。

自從 V8 5.9版本發(fā)布以來脂崔, V8 就不再在 JavaScript 執(zhí)行里使用 full-codegenCrankshaft(自2010年來一直支撐著 V8 的技術(shù)),這是由于 V8 團(tuán)隊也在努力地跟上新的 JavaScript 語言特性的腳步和這些特性所需的優(yōu)化屁擅。

這意味著將來在整體上 V8 將擁有更加簡單和更易于維護(hù)的架構(gòu)。

這些提升僅僅是個開始早抠。新的 IgnitionTurboFan 管道鋪墊了更遠(yuǎn)的優(yōu)化之路吼过,將會推進(jìn) JavaScript 的性能并在接下來的幾年里縮小 V8ChromeNode.js 中的足跡羊赵。

最后,這里有幾條關(guān)于如何編寫更優(yōu)化的、更好的 JavaScript 代碼的建議和技巧。雖然你可以很容易地從上述的內(nèi)容中得到這些,為了方便還是把它們做了以下的總結(jié):

怎么編寫優(yōu)化的JavaScript

  1. 對象屬性的順序:始終使用相同的順序初始化對象屬性劲绪,以便共享隱藏類和隨后的優(yōu)化代碼祷安。
  2. 動態(tài)屬性:在初始化完成之后添加對象動態(tài)屬性會強(qiáng)制改變隱藏類并使之前的隱藏類已優(yōu)化的方法變慢汇鞭。相反淡溯,在對象的構(gòu)造器里指定所有的屬性强品。
  3. 方法:重復(fù)執(zhí)行相同方法的代碼會比僅執(zhí)行一次許多不同的方法運(yùn)行的更快(由于內(nèi)聯(lián)緩存)夫晌。
  4. 數(shù)組:避免使用鍵值不遞增的稀疏數(shù)組盏档。并非每個元素都存在的稀疏數(shù)組是一個哈希表。訪問稀疏數(shù)組的元素將會花費(fèi)更昂貴的開銷鱼填。此外宦言,避免預(yù)先分配大數(shù)組响疚。最好是按需要增加長度鸦采。最后咱旱,不要刪除數(shù)組中的元素吐限。這會使數(shù)組變得稀疏。
  5. 帶標(biāo)記的值V8 用32位字節(jié)表示對象和數(shù)字诸典。其中使用了一個位來標(biāo)識是對象(標(biāo)識為1)或是整數(shù)(標(biāo)識為0)蒋搜,由于它們是31位的而被稱為 SMISMall Integer)豆挽。如果一個數(shù)值大小超過了31位可以表示的數(shù)字,V8 將會包裝它私蕾,將其轉(zhuǎn)換為一個雙字節(jié)類型值并創(chuàng)建一個新的對象存入其中。盡量使用31帶符號的數(shù)值避免 JS 對象的昂貴包裝操作。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖锰蓬,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異匆帚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)骑疆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門田篇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人箍铭,你說我怎么就攤上這事泊柬。” “怎么了坡疼?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵彬呻,是天一觀的道長。 經(jīng)常有香客問我柄瑰,道長闸氮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任教沾,我火速辦了婚禮蒲跨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘授翻。我一直安慰自己或悲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布堪唐。 她就那樣靜靜地躺著巡语,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淮菠。 梳的紋絲不亂的頭發(fā)上男公,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音合陵,去河邊找鬼枢赔。 笑死,一個胖子當(dāng)著我的面吹牛拥知,可吹牛的內(nèi)容都是我干的踏拜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼低剔,長吁一口氣:“原來是場噩夢啊……” “哼速梗!你這毒婦竟也來了肮塞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤镀琉,失蹤者是張志新(化名)和其女友劉穎峦嗤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屋摔,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烁设,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了钓试。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片装黑。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖弓熏,靈堂內(nèi)的尸體忽然破棺而出恋谭,到底是詐尸還是另有隱情,我是刑警寧澤挽鞠,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布疚颊,位于F島的核電站,受9級特大地震影響信认,放射性物質(zhì)發(fā)生泄漏材义。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一嫁赏、第九天 我趴在偏房一處隱蔽的房頂上張望其掂。 院中可真熱鬧,春花似錦潦蝇、人聲如沸款熬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贤牛。三九已至,卻和暖如春则酝,著一層夾襖步出監(jiān)牢的瞬間盔夜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工堤魁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人返十。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓妥泉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親洞坑。 傳聞我的和親對象是個殘疾皇子盲链,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容