App下載

【譯】深入了解 JavaScript V8

猿友 2020-09-15 11:38:40 瀏覽數(shù) (3244)
反饋

文章來(lái)源于公眾號(hào):符合預(yù)期的CoyPan ,作者CoyPan

原文標(biāo)題:A Deep Dive Into V8

原文鏈接:blog.appsignal.com/2020/07/01/a-deep-dive-into-v8.html?utm_source=javascript-weekly-sponsored&utm_medium=email&utm_campaign=deep-dive-v8&utm_content=sponsored-link

正文開(kāi)始

大部分前端開(kāi)發(fā)人員都會(huì)遇到一個(gè)流行詞:V8。它的流行程度很大一部分是因?yàn)樗鼘?JavaScript 的性能提升到了一個(gè)新的水平。

是的,V8很快。但它是如何發(fā)揮它的魔力?為什么它反應(yīng)如此迅速呢?

官方文檔指出: V8 是谷歌開(kāi)源高性能 JavaScriptWebAssembly 引擎,用 C++ 編寫。它主要用在 ChromeNode.js中,等等。

換句話說(shuō),V8是一種C++開(kāi)發(fā)的軟件,它將 JavaScript 編譯成可執(zhí)行代碼,即機(jī)器碼。

現(xiàn)在,我們開(kāi)始看得更清楚,ChromeNode.js只是一個(gè)橋梁,負(fù)責(zé)把 JS 代碼運(yùn)送到最終的目的地:在特定機(jī)器上運(yùn)行的機(jī)器碼。

V8性能的另一個(gè)重要角色是它的分代和超精確的垃圾收集器。它被優(yōu)化為使用低內(nèi)存收集 JavaScript 不再需要的對(duì)象。

除此之外,V8 還依靠一組其他的工具和特性來(lái)改進(jìn) JS 的一些固有功能。這些功能往往會(huì)使 JS 變慢(例如JS的動(dòng)態(tài)特性)。

在本文中,我們將更詳細(xì)地探討這些工具(Ignition 和 TurboFan)和特性。除此之外,我們還將介紹V8的內(nèi)部功能、編譯和垃圾回收過(guò)程、單線程特性等基礎(chǔ)知識(shí)。

從基礎(chǔ)的開(kāi)始

機(jī)器碼是如何工作的呢?簡(jiǎn)單地說(shuō),機(jī)器代碼是在機(jī)器內(nèi)存的特定部分執(zhí)行的一組非常低級(jí)的指令。

生成機(jī)器碼的過(guò)程,用C++舉例,大概像下面這樣:

生成機(jī)器碼的過(guò)程

在進(jìn)一步討論之前,必須指出這是一個(gè)編譯過(guò)程,它不同于 JavaScript 解釋過(guò)程。實(shí)際上,編譯器在進(jìn)程結(jié)束時(shí)生成一個(gè)完整的程序,而解釋器作為一個(gè)程序本身工作,它通過(guò)讀取指令(通常是腳本,如JavaScript腳本)并將其轉(zhuǎn)換為可執(zhí)行命令來(lái)完成任務(wù)。

解釋過(guò)程可以是動(dòng)態(tài)的(解釋器解析并只運(yùn)行當(dāng)前命令)或完全解析(即解釋器在繼續(xù)執(zhí)行相應(yīng)的機(jī)器指令之前首先完全翻譯腳本)。

回到圖中,編譯過(guò)程通常從源代碼開(kāi)始。你實(shí)現(xiàn)代碼,保存并運(yùn)行。運(yùn)行的進(jìn)程依次從編譯器開(kāi)始。編譯器是一個(gè)程序,和其他程序一樣,運(yùn)行在你的機(jī)器上。然后它遍歷所有代碼并生成對(duì)象文件。那些文件是機(jī)器代碼。它們是在特定機(jī)器上運(yùn)行的優(yōu)化代碼,這就是為什么當(dāng)你從一個(gè)操作系統(tǒng)轉(zhuǎn)移到另一個(gè)操作系統(tǒng)時(shí)必須使用特定的編譯器。

但是你不能執(zhí)行單獨(dú)的對(duì)象文件,你需要把它們組合成一個(gè)文件,即眾所周知的.exe文件(可執(zhí)行文件)。這是Linker的工作。

最后,Loader是代理,負(fù)責(zé)將 exe 文件中的代碼傳輸?shù)讲僮飨到y(tǒng)的虛擬內(nèi)存中。它基本上是一個(gè)運(yùn)輸工具。在這里,你的程序終于開(kāi)始運(yùn)行了。

聽(tīng)起來(lái)是一個(gè)漫長(zhǎng)的過(guò)程,不是嗎?

大多數(shù)時(shí)候(除非你是在銀行大型機(jī)上使用匯編的開(kāi)發(fā)人員),你會(huì)花時(shí)間用高級(jí)語(yǔ)言編程:Java、C#、Ruby、JavaScript等。

語(yǔ)言越高級(jí),速度越慢。這就是為什么CC++速度更快,因?yàn)樗鼈兎浅=咏鼨C(jī)器代碼語(yǔ)言:匯編語(yǔ)言。

除了性能之外,V8的主要優(yōu)點(diǎn)之一是超越ECMAScript標(biāo)準(zhǔn)的可能性,并且理解C++。

V8`優(yōu)點(diǎn)超越ECMAScript

JavaScript 僅限于ECMAScript。而V8引擎,為了存在,必須是兼容的,但不限于 JavaScript 。

具有將C++特性集成到V8中的能力是非常棒的。由于C++已經(jīng)發(fā)展到非常好的 OS 操作的文件處理和內(nèi)存/線程處理的特殊性——在 JavaScript 中擁有所有這些能力是非常有用的。

如果你仔細(xì)想想,Node.js它本身也是以類似的方式誕生的。它遵循與V8相似的路徑,外加服務(wù)器和網(wǎng)絡(luò)功能。

單線程

如果你是一個(gè)Node開(kāi)發(fā)者,你應(yīng)該很熟悉V8的單線程特性。一個(gè) JS 執(zhí)行上下文與線程數(shù)量成正比。

當(dāng)然,V8在后臺(tái)管理操作系統(tǒng)線程機(jī)制。它可以與多個(gè)線程一起工作,因?yàn)樗且粋€(gè)復(fù)雜的軟件,可以同時(shí)執(zhí)行許多任務(wù)。

但是,V8為每個(gè) JavaScript 的執(zhí)行上下文只創(chuàng)建一個(gè)單線程的環(huán)境。其余的都在V8的控制之下。

想象一下 JavaScript 代碼應(yīng)該進(jìn)行的函數(shù)調(diào)用堆棧。 JavaScript 的工作原理是將一個(gè)函數(shù)堆疊在另一個(gè)函數(shù)之上,遵循每個(gè)函數(shù)的插入/調(diào)用順序。在到達(dá)每個(gè)函數(shù)的內(nèi)容之前,我們無(wú)法知道它是否調(diào)用其他函數(shù)。如果發(fā)生這種情況,那么被調(diào)用的函數(shù)將被放在堆棧中調(diào)用者的后面。

例如,當(dāng)涉及回調(diào)時(shí),它們被放在堆棧的末尾。

管理這個(gè)堆棧組織和進(jìn)程所需的內(nèi)存是V8的主要任務(wù)之一。

Ignition and TurboFan

自2017年5月發(fā)布的5.9版以來(lái),V8 附帶了一個(gè)新的JavaScript執(zhí)行管道,它構(gòu)建在V8的解釋器Ignition之上。它還包括一個(gè)更新和更好的優(yōu)化編譯器-TurboFan。

這些變化完全集中在整體性能上,以及 Google 開(kāi)發(fā)人員在調(diào)整引擎以適應(yīng) JavaScript 領(lǐng)域帶來(lái)的所有快速而顯著的變化時(shí)所面臨的困難。

從項(xiàng)目一開(kāi)始,V8的維護(hù)人員就一直在擔(dān)心如何在 JavaScript 不斷發(fā)展的同時(shí),找到一種提高V8性能的好方法。

現(xiàn)在,我們可以看到新引擎的Benchmarks測(cè)試結(jié)果,已經(jīng)有了巨大提升:

新引擎Benchmarks測(cè)試結(jié)果

Hidden Classes(隱藏類)

這是V8的另一個(gè)魔術(shù)。JavaScript 是一種動(dòng)態(tài)語(yǔ)言。這意味著可以在執(zhí)行期間添加、替換和刪除新屬性。例如,在Java這樣的語(yǔ)言中,這是不可能的,在Java中,所有的東西(類、方法、對(duì)象和變量)都必須在程序執(zhí)行之前定義,并且在應(yīng)用程序啟動(dòng)后不能動(dòng)態(tài)更改。

由于它的特殊性質(zhì),JavaScript 解釋器通?;谏⒘泻瘮?shù)(hash算法)執(zhí)行字典查找,以準(zhǔn)確地知道這個(gè)變量或?qū)ο笤趦?nèi)存中的分配位置。

這對(duì)最后一道工序來(lái)說(shuō)代價(jià)很大。在其他語(yǔ)言中,當(dāng)對(duì)象被創(chuàng)建時(shí),它們接收一個(gè)地址(指針)作為其隱式屬性之一。這樣,我們就可以準(zhǔn)確地知道它們?cè)趦?nèi)存中的位置以及要分配多少空間。

對(duì)于 JavaScript,這是不可能的,因?yàn)槲覀儫o(wú)法映射出不存在的內(nèi)容。這就是Hidden Classes發(fā)揮作用的地方。

隱藏類與Java中的類幾乎相同:靜態(tài)類和固定類具有唯一的地址來(lái)定位它們。然而,V8并不是在程序執(zhí)行之前執(zhí)行,而是在運(yùn)行過(guò)程中,每次對(duì)象結(jié)構(gòu)發(fā)生“動(dòng)態(tài)變化”時(shí)執(zhí)行。

讓我們看一個(gè)例子來(lái)說(shuō)明問(wèn)題。考慮以下代碼片段:

function User(name, fone, address) {
   this.name = name
   this.phone = phone
   this.address = address
}

在 JavaScript 基于原型的特性中,每次實(shí)例化一個(gè)新的用戶對(duì)象時(shí),假設(shè):

var user = new User("John May", "+1 (555) 555-1234", "123 3rd Ave")

然后V8創(chuàng)建一個(gè)新的隱藏類。我們稱之為_User0

創(chuàng)建新的隱藏類_User0

每個(gè)對(duì)象在內(nèi)存中都有一個(gè)對(duì)其類表示的引用。它是類指針。此時(shí),由于我們剛剛實(shí)例化了一個(gè)新對(duì)象,所以在內(nèi)存中只創(chuàng)建了一個(gè)隱藏類?,F(xiàn)在是空的。

當(dāng)你在這個(gè)函數(shù)中執(zhí)行第一行代碼時(shí),將在上一個(gè)基礎(chǔ)上創(chuàng)建一個(gè)新的隱藏類,這次是_User1

創(chuàng)建新的隱藏類_User1

它基本上是具有name屬性的User的內(nèi)存地址。在我們的示例中,我們沒(méi)有使用僅將name作為屬性的user,但每次這樣做時(shí),這就是V8將作為引用加載的隱藏類。

name屬性被添加到內(nèi)存緩沖區(qū)的偏移量 0,這意味著這將被視為最后順序中的第一個(gè)屬性。

V8還將向_User0隱藏類添加一個(gè)轉(zhuǎn)換值。這有助于解釋器理解:每次向User對(duì)象添加name屬性時(shí),必須處理從_User0_User1的轉(zhuǎn)換。

當(dāng)調(diào)用函數(shù)中的第二行時(shí),同樣的過(guò)程再次發(fā)生,并創(chuàng)建一個(gè)新的隱藏類:

創(chuàng)建一個(gè)新的隱藏類

你可以看到隱藏類跟蹤堆棧。在由轉(zhuǎn)換值維護(hù)的鏈中,一個(gè)隱藏類通向另一個(gè)。

屬性添加的順序決定了V8將要?jiǎng)?chuàng)建多少個(gè)隱藏類。如果您更改我們所創(chuàng)建的代碼段中的行的順序,那么也將創(chuàng)建不同的隱藏類。這就是為什么有些開(kāi)發(fā)人員試圖保持重用隱藏類的順序,從而減少開(kāi)銷。

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

這是JIT(Just-in-Time)編譯器中非常常見(jiàn)的一個(gè)術(shù)語(yǔ)。它與隱藏類的概念直接相關(guān)。

例如,每當(dāng)你調(diào)用一個(gè)函數(shù),將一個(gè)對(duì)象作為參數(shù)傳遞時(shí),V8會(huì)看到這個(gè)動(dòng)作,然后想:“嗯,這個(gè)對(duì)象作為參數(shù)成功地傳遞了兩次或更多次……為什么不把它存儲(chǔ)在我的緩存中以備將來(lái)調(diào)用,而不是再次執(zhí)行整個(gè)耗時(shí)的隱藏類驗(yàn)證過(guò)程?”

讓我們回顧上一個(gè)例子:

function User(name, fone, address) { // Hidden class _User0
   this.name = name // Hidden class _User1
   this.phone = phone // Hidden class _User2
   this.address = address // Hidden class _User3
}

當(dāng)我們將 User 對(duì)象的實(shí)例兩次作為參數(shù)傳遞給函數(shù)后,V8將跳轉(zhuǎn)到隱藏類查找并直接轉(zhuǎn)到偏移量的屬性。這要快得多。

但是,請(qǐng)記住,如果更改函數(shù)中任何屬性賦值的順序,則會(huì)導(dǎo)致不同的隱藏類,因此V8將無(wú)法使用內(nèi)聯(lián)緩存功能。

這是一個(gè)很好的例子,說(shuō)明開(kāi)發(fā)人員不應(yīng)該避免更深入地了解引擎。相反,擁有這些知識(shí)將有助于代碼更好地執(zhí)行。

Garbage Collecting(垃圾回收)

你還記得我們提到過(guò)V8在另一個(gè)線程中收集內(nèi)存垃圾嗎?這很有幫助,因?yàn)槲覀兊某绦驁?zhí)行不會(huì)受到影響。

V8使用眾所周知的“標(biāo)記和掃描”策略來(lái)收集內(nèi)存中的舊對(duì)象。在這種策略中,GC掃描內(nèi)存對(duì)象以“標(biāo)記”它們以進(jìn)行收集的階段有點(diǎn)慢,因?yàn)檫@需要暫停代碼執(zhí)行。

但是,V8是遞增的,也就是說(shuō),對(duì)于每個(gè) GC 停頓,V8嘗試標(biāo)記盡可能多的對(duì)象。它使一切變得更快,因?yàn)樵诩贤瓿芍安恍枰V拐麄€(gè)執(zhí)行。在大型應(yīng)用程序中,性能的提高有很大的不同。

以上就是W3Cschool編程獅關(guān)于【譯】深入了解JavaScript V8的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊