深刻討論 V8

v8-weixin.png

原文做者:Diogo Souza

原文連接:https://blog.appsignal.com/20...javascript

大多數前端開發人員一直在討論這個時髦詞:V8。它的流行很大程度上是由於它將 JavaScript 的性能提高到了一個新的水平。html

是的,V8很是快。 可是,它是如何發揮其魔力的,爲何它會如此敏捷?前端

官方文檔指出,「 V8是用 C ++ 編寫的Google的開源高性能JavaScript和WebAssembly引擎。 它用於Chrome 和 Node.js 等。」java

換句話說,V8是一個用c++開發的軟件,能夠將JavaScript轉換成可執行機器代碼。c++

Google Chrome和Node.js都只是將JavaScript代碼傳輸到其最終目的地的橋樑:在該特定機器上運行的機器代碼。編程

V8性能表現中的另外一個重要角色是它的分代和超級精確的垃圾收集器。 它通過優化,收集JavaScript再也不須要的對象,使得佔用內存低。緩存

除此以外,V8還依靠其餘工具和功能來改善某些固有的 JavaScript 功能,這些功能在過去會使語言變慢(例如,它的動態特性)。服務器

在本文中,咱們將更詳細地探討這些工具(Ignition 和 TurboFan)及其功能。 除此以外,咱們還將介紹V8的內部功能,編譯和垃圾回收過程,單線程性質等基礎知識。網絡

一、從基礎開始

機器代碼如何工做? 簡而言之,機器代碼是一堆很是低級的指令,它們在機器內存的特定部分中執行。app

使用 c++ 語言做爲參考生成它與它相似的功能:

figure01.png

在進一步討論以前,必須指出這是一個編譯過程,它與 JavaScript 解釋過程不一樣。實際上,編譯器在過程結束時生成整個程序,而解釋器做爲程序自己來工做,它經過讀取指令(一般做爲腳本,好比JavaScript腳本)並將它們翻譯成可執行命令來完成這項工做。

解釋過程能夠是動態的(解釋器解析並只運行當前命令),也能夠是徹底解析的(即解釋器在繼續執行各自的機器指令以前首先徹底翻譯腳本)。

回到圖中,正如所見,編譯過程一般從源代碼開始。實現代碼,保存並運行。運行的進程依次從編譯器開始。編譯器是一個程序,像任何其餘程序同樣,運行在您的機器上。而後遍歷全部代碼並生成目標文件。那些文件就是機器代碼。他們優化了運行在特定機器上的代碼,這就是爲何當你從一個操做系統移動到另外一個操做系統時,你必須使用特定的編譯器。

可是您不能執行單獨的目標文件,您須要將它們合併成一個單獨的文件,即衆所周知的.exe文件(可執行文件)。那是連接器(linker)的工做。

最後,加載器是負責將exe文件中的代碼傳輸到操做系統虛擬內存的代理。它基本上是一個轉運體。在這裏,您的程序終於啓動並運行了。

聽起來像是一個漫長的過程,不是嗎?

在大多數狀況下(除非您是在銀行大型機中使用Assembly的開發人員),您都將花費時間用高級語言進行編程:Java,C#,Ruby,JavaScript等。

語言越高,速度越慢。這就是爲何 C 和 C++ 要快得多,它們很是接近機器代碼語言:彙編語言。

除了性能以外,V8的一個主要好處是能夠超越ECMAScript標準,也能夠理解c++

figure02.png

JavaScript 受限於 ECMAScript。而V8爲了生存,就必須兼容但不限於它。

在V8中具備很是棒的集成C++特性的能力。C++已經發展的很是好的OS操做:文件處理和內存/線程處理,在JavaScript中擁有全部這些能力是很是有用的。

若是您仔細想一想,Node.js自己就是以相似的方式誕生的。 它採用了相似的方式來升級到V8,再加上服務器和網絡功能。

二、單線程

若是您是Node開發人員,那麼您應該很清楚V8的單線程性質。 每一個JavaScript執行上下文都與一個線程成正比。

固然,V8在後臺管理OS線程機制。 它是一個複雜的軟件,而且能夠同時執行許多工做,所以可使用多個線程。

咱們有一個執行代碼的主線程,另外一個用於編譯代碼的線程(是的,每次編譯新代碼時咱們都沒法中止執行),還有一些用於處理垃圾回收,等等。

然而,V8爲每一個JavaScript執行上下文建立了一個單線程環境。其他的都在它的控制之下。

想象一下您應該執行JavaScript代碼的函數調用棧。 JavaScript經過按照插入/調用每一個函數的順序將一個函數堆疊在另外一個函數之上來工做。 在介紹每一個功能的內容以前,咱們沒法知道它是否調用了其餘功能。 若是發生這種狀況,那麼被調用的函數將被放置在堆棧中調用者以後。

例如,當涉及到回調時,它們被放置在堆棧的末尾。

V8的主要任務之一是管理該堆棧組織和該過程所需的內存。

三、Ignition 和 TurboFan

自2017年5月發佈5.9版以來,V8附帶了一個新的JavaScript執行管道,該管道基於V8的解釋器Ignition構建。 它還包括更新更好的優化編譯器⁠-TurboFan。

這些變化徹底集中在總體性能上,以及Google開發人員在調整引擎以適應JavaScript領域帶來的全部快速而顯著的變化時所面臨的困難。

從項目一開始,V8的維護者就一直在擔憂可否找到一種好方法來提升V8的性能,使其與JavaScript的發展速度保持一致。

如今咱們能夠看到,在運行新引擎時,與最大的基準測試相比,有了巨大的改進

figure03.png

來源: https://v8.dev/blog/launching...*

你能夠在這裏和這裏閱讀更多關於 Ignition and TurboFan

四、隱藏類

這是V8的另外一個魔術。JavaScript是一種動態語言。這意味着能夠在執行期間添加、替換和刪除新屬性。這在Java這樣的語言中是不可能的,例如,全部的東西(類、方法、對象和變量)都必須在程序執行以前定義,而且不能在應用程序啓動後動態更改。

因爲其特殊的性質,JavaScript解釋器一般根據散列函數執行字典查找,以確切知道該變量或對象在內存中的分配位置。

這在最終過程當中花費不少。 在其餘語言中,建立對象時,它們會收到一個地址(指針)做爲其隱式屬性之一。 這樣,咱們就確切知道它們在內存中的放置位置以及要分配的空間。

使用 JavaScript,這是不可能的,由於咱們不能映射不存在的東西。這就是隱藏類統治的地方。

隱藏類幾乎和 Java 中的同樣:靜態類和固定類都有一個惟一的地址來定位它們。然而,V8並非在程序執行以前執行,而是在運行時執行,每當對象結構發生動態變化時。

讓咱們看一個例子來明白問題。 考慮如下代碼片斷:

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

在 JavaScript 基於原型的性質下,每次咱們實例化一個新的 User 對象時,能夠說:

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

而後,V8建立一個新的隱藏類。 咱們稱它爲_User0。

figure04.png

每一個對象在內存中都有一個對其類表示的引用。它是類指針。此時,由於咱們剛剛實例化了一個新對象,因此在內存中只建立了一個隱藏類。它如今是空的。

當您執行此函數中的第一行代碼時,將基於上一個隱藏類(此次是 _User1)建立一個新的隱藏類。

figure05.png

基本上,它是具備name 屬性的 User 的內存地址。 在咱們的示例中,咱們並無使用僅具備名稱的用戶做爲屬性,可是每次您使用它時,都會加載隱藏的類V8做爲參考。

name 屬性被添加到內存緩衝區的偏移量0中,這意味着它將被視爲最終順序中的第一個屬性。

V8 will also add a transition value to the _User0 hidden class. This helps the interpreter to understand that every time a name property is added to a User object, the transition from _User0 to _User1 must be addressed.

V8還將向_User0隱藏類添加一個過渡值。 這有助於解釋器理解,每次將name屬性添加到User對象時,都必須吹裏從_User0_User1的轉換。

當調用函數的第二行時,相同的過程再次發生,並建立一個新的隱藏類:

figure06.png

您能夠看到,隱藏類跟蹤堆棧。在由轉換值維護的鏈中,一個隱藏類致使另外一個隱藏類。

屬性添加的順序決定了V8將要建立多少隱藏類。若是您更改咱們建立的代碼片斷中的行順序,還將建立不一樣的隱藏類。這就是爲何一些開發人員試圖維護重用隱藏類的順序,從而減小開銷。

五、高速緩存

這是 JIT(Just In Time)編譯器世界中很是廣泛的術語。 它與隱藏類的概念直接相關。

例如,每次調用將對象做爲參數傳遞的函數時,V8都會查看此操做並思考:「嗯,該對象已成功兩次或屢次做爲參數傳遞給該函數……爲何不將其存儲在 個人緩存用於未來的調用,而不是再次執行整個耗時的隱藏類驗證過程?」

讓咱們回顧一下最後一個例子:

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
}

在將實例化的任意值做爲參數的用戶對象兩次發送給一個函數後,V8將跳過隱藏類查找並直接轉到偏移量的屬性。這要快得多。

可是,請記住,若是在函數中更改任何屬性分配的順序,則會產生不一樣的隱藏類,所以V8將沒法使用高速緩存功能。

這是一個很好的例子,說明開發人員不該該不更深刻地瞭解引擎。相反,擁有這些知識將幫助您的代碼執行得更好。

六、垃圾回收

你還記得咱們提到的V8在不一樣的線程中收集內存垃圾嗎?因此,這頗有幫助,由於咱們的程序執行不會受到影響。

V8使用衆所周知的「標記-清除策略」來收集內存中的死對象和舊對象。在這種策略中,GC掃描內存對象並將其標記爲收集的階段有點慢,由於它暫停執行以實現收集。

然而,V8是增量執行的。對於每一個GC中止,V8嘗試標記儘量多的對象。它使一切都更快,由於在收集完成以前不須要中止整個執行。在大型應用程序中,性能改進會帶來很大的不一樣。

相關文章
相關標籤/搜索