為什么叫V8不叫V9?究竟是怎么工作的?
前端開發(fā)人員都會(huì)遇到一個(gè)流行詞:V8。
它的流行程度很大一部分是因?yàn)樗鼘avaScript的性能提升到了一個(gè)新的水平。是的,V8很快。但它是如何發(fā)揮它的魔力?
前言

源代碼:https://source.chromium.org/chromium/chromium/src/+/master:v8/ [1]
????在介紹V8引擎之前,我們可以先分析下為什么需要V8引擎?眾所周知,前端最火的開發(fā)語言非javascript莫屬,那javascript與V8是什么樣的關(guān)系呢?
我們知道,計(jì)算機(jī)只能識(shí)別二進(jìn)制的機(jī)器語言,無法識(shí)別更高級(jí)的語言,用高級(jí)的語言開發(fā),需要先將這些語言翻譯成機(jī)器語言,而語言種類大體可以分為解釋型語言和編譯型語言:
| 語言種類 | 翻譯過程 | 優(yōu)點(diǎn) | 不足 | 常見語言例子 |
|---|---|---|---|---|
| 解釋型語言 | 解釋器 > 翻譯成與平臺(tái)無關(guān)的中間代碼 | 與平臺(tái)無關(guān),跨平臺(tái)性強(qiáng) | 每次都需要解釋執(zhí)行 需要源文件 按句執(zhí)行,執(zhí)行效率差 | javascript、Ruby、Python |
| 編譯型語言 | 預(yù)處理>編譯>匯編>可執(zhí)行的二進(jìn)制文件 | 一次編譯,永久執(zhí)行 無需源代碼,只需要可執(zhí)行的源文件 運(yùn)行速度快 | 不同系統(tǒng)可識(shí)別的二進(jìn)制文件不同,跨平臺(tái)兼容性差 | C、C++、java |
??? JavaScript就是一種解釋型語言,支持動(dòng)態(tài)類型、弱類型、基于原型的語言,內(nèi)置支持類型。一般JavaScript都是在前端側(cè)執(zhí)行,需要能快速響應(yīng)用戶,所以這就要求語言本身可以被快速地解析和執(zhí)行,javascript引擎就為此目的而生。
JS引擎歷史那些事
????1993年網(wǎng)景瀏覽器誕生,成為瀏覽器鼻祖。
????1995年微軟推出了IE瀏覽器,拉開了第一次瀏覽器大戰(zhàn)的序幕。IE受益于windows系統(tǒng)風(fēng)靡世界,逐漸占有了大部分市場。
????1998年處于低谷的網(wǎng)景公司成立了Mozilla基金會(huì),在該基金會(huì)推動(dòng)下,開發(fā)了著名的開源項(xiàng)目Firefox并在2004年發(fā)布1.0版本,拉開了第二次瀏覽器大戰(zhàn)的序幕,IE發(fā)展更新較緩慢,F(xiàn)irefox一推出就深受大家的喜愛,市場份額一直上升。
????2003年,蘋果發(fā)布了Safari瀏覽器,并在2005年釋放了瀏覽器中一種非常重要部件的源代碼,發(fā)起了一個(gè)新的開源項(xiàng)目WebKit。
????2008年,Google以蘋果開源項(xiàng)目WebKit作為內(nèi)核,創(chuàng)建了一個(gè)新的項(xiàng)目Chromium,在Chromium的基礎(chǔ)上,Google發(fā)布了Chrome瀏覽器。Google工程師起初曾考慮過使用 Firefox 的 Gecko 內(nèi)核,然而他們最終被 Android 團(tuán)隊(duì)說服采用了 WebKit,他們勸說道:WebKit 輕快、易擴(kuò)展、代碼結(jié)構(gòu)清晰。而且在蘋果公司內(nèi)部不停的速度壓榨的情況下,最終才發(fā)布了WebKit并把它開源。https://www.google.com/googlebooks/chrome/med_14.html [2]
????站在當(dāng)下回頭來看,當(dāng)時(shí)的選擇無比的明智,現(xiàn)在WebKit 更是出現(xiàn)在幾乎每一個(gè)移動(dòng)平臺(tái)——iOS、Android、BlackBerry等等??上У氖牵珿oogle加入了WebKit之列并成為開發(fā)的主力后,獨(dú)立fork出了Blink,自己的內(nèi)核,又與WebKit分道揚(yáng)鑣了。
????????在Blink基礎(chǔ)之上,為了追求javascript的極致速度和性能,Google工程師又創(chuàng)造出來了V8引擎,而Node的作者認(rèn)為這么優(yōu)秀的引擎只在瀏覽器中跑可惜了,不如拿出來寫一些配套的模塊,就可以開發(fā)服務(wù)器端應(yīng)用了,這就有了Node.js,至此javascript語言從單純的前端語言,蛻變成了一門全端語言,從而有了全棧工程師的發(fā)展方向。
????微軟依然維護(hù)著自己的EdgeHTML引擎,作為老的Trident引擎的替代方案。新的Edge的瀏覽器已經(jīng)開始使用Chromium的Blink引擎了,當(dāng)Edge加入Blink的陣營后,以Webkit和其衍生產(chǎn)品已經(jīng)牢牢的占有市場的6成以上。

什么是V8引擎
V8引擎是一個(gè)JavaScript引擎實(shí)現(xiàn),最初由一些語言方面專家設(shè)計(jì),后被谷歌收購,隨后谷歌對(duì)其進(jìn)行了開源。
V8的名字來源于汽車的“V型8缸發(fā)動(dòng)機(jī)”,使用C++開發(fā),在運(yùn)行JavaScript之前,相比其它的JavaScript的引擎轉(zhuǎn)換成字節(jié)碼或解釋執(zhí)行,V8將其編譯成原生機(jī)器碼(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如內(nèi)聯(lián)緩存(inline caching)等方法來提高性能。
有了這些功能,JavaScript程序在V8引擎下的運(yùn)行速度媲美二進(jìn)制程序。
V8支持眾多操作系統(tǒng),如windows、linux、android等,也支持其他硬件架構(gòu),如IA32,X64,ARM等,具有很好的可移植和跨平臺(tái)特性。
V8如何運(yùn)行Javascript

Loading
??? js文件加載的過程并不是由V8負(fù)責(zé)的,它可能來自于網(wǎng)絡(luò)請(qǐng)求、本地的cache或者是也可以是來自service worker,瀏覽器的js加載的整個(gè)過程,就是V8引擎運(yùn)行js的前置步驟。
3種加載方式 & V8的優(yōu)化:
Cold load: 首次加載腳本文件時(shí),沒有任何數(shù)據(jù)緩存 Warm load:V8分析到如果使用了相同的腳本文件,會(huì)將編譯后的代碼與腳本文件一起緩存到磁盤緩存中 Hot load: 當(dāng)?shù)谌渭虞d相同的腳本文件時(shí),V8可以從磁盤緩存中載入腳本,并且還能拿到上次加載時(shí)編譯后的代碼,這樣可以避免完全從頭開始解析和編譯腳本
????延伸閱讀:V8 6.6 進(jìn)一步改進(jìn)緩存性能[3] 代碼緩存策略優(yōu)化,簡單講就是從緩存代碼依賴編譯過程的模式,改變成兩個(gè)過程解耦,并增加了可緩存的代碼量,從而提升了解析和編譯的時(shí)間
【舊模式】 
?【新模式】 
Parsing
????Parsing(分析過程)是將js腳本轉(zhuǎn)換成AST(抽象語法樹:Abstract Syntax Tree)的過程
詞法分析
Token
????從左往右逐個(gè)字符地掃描源代碼,通過分析,產(chǎn)生一個(gè)不同的標(biāo)記,這里的標(biāo)記稱為token,代表著源代碼的最小單位,通俗講就是將一段代碼拆分成最小的不可再拆分的單元,這個(gè)過程稱為詞法標(biāo)記。詞法分析器常用的token標(biāo)記種類有幾類:
常數(shù)(整數(shù)、小數(shù)、字符、字符串等) 操作符(算術(shù)操作符、比較操作符、邏輯操作符) 分隔符(逗號(hào)、分號(hào)、括號(hào)等) 保留字 標(biāo)識(shí)符(變量名、函數(shù)名、類名等)
TOKEN-TYPE?TOKEN-VALUE\
-----------------------------------------------\
T_IF?????????????????if\
T_WHILE??????????????while\
T_ASSIGN?????????????=\
T_GREATTHAN??????????>\
T_GREATEQUAL?????????>=\
T_IDENTIFIER?name????/?numTickets?/?...\
T_INTEGERCONSTANT????100?/?1?/?12?/?....\
T_STRINGCONSTANT?????"This?is?a?string"?/?"hello"?/?...
流式處理
????詞法處理過程中,輸入是字節(jié)流,輸出是Token流:
????定義100行單詞處理時(shí)間1t,占用內(nèi)存1m,來對(duì)比看下流式和非流式兩種方式的區(qū)別:
非流式處理: 時(shí)間消耗6t,內(nèi)存消耗峰值3M,讀取+處理完成才會(huì)釋放內(nèi)存塊; 流式處理: 時(shí)間消耗是4t,內(nèi)存消耗峰值1M,內(nèi)存釋放相同機(jī)制;
????可以發(fā)現(xiàn)流式處理能很大程度上提升處理效率和節(jié)省內(nèi)存空間
詞法分析器結(jié)構(gòu)

????掃描緩存區(qū)大小一般是固定的大小,但也無法確保能不影響單詞的邊界,同時(shí)會(huì)使用兩個(gè)指示器(指針),一個(gè)指向正在識(shí)別的單詞頭部,一個(gè)向前搜索單詞的終點(diǎn)。
????這里有個(gè)概念是:超前搜索,用戶可能把關(guān)鍵字等特殊的語句重新定義,所以掃描器需要提前掃描到這些字后面的代碼格式,才能確定其最終代表的詞性。有這個(gè)問題存在會(huì)影響到性能,現(xiàn)在的大多數(shù)語言會(huì)把基本字都設(shè)置成保留字,用戶不能修改其含義。
有限狀態(tài)機(jī)
????掃描器依托有限狀態(tài)機(jī)來實(shí)現(xiàn)正則匹配不同的token類型,下面以識(shí)別整數(shù)和浮點(diǎn)數(shù)的狀態(tài)機(jī)為例,簡單講下過程:
單圓代表著“結(jié)點(diǎn)”,代表著掃描過程中可能出現(xiàn)的“狀態(tài)”,也就是上面提到的兩個(gè)指示器中間對(duì)應(yīng)的所有字符;
箭頭指向可稱為“邊”,從一個(gè)狀態(tài)指向另外一個(gè)狀態(tài),邊的標(biāo)號(hào)代表一個(gè)或者多個(gè)符號(hào),如果能匹配一條邊,向前指針就會(huì)前移,指向下個(gè)狀態(tài)
Start代表著開始狀態(tài)
雙圓環(huán)代表著接受狀態(tài)或者最終狀態(tài),代表著已經(jīng)找到了準(zhǔn)確的狀態(tài),向語法分析器返回一個(gè)token和相關(guān)的屬性值
“*”代表著可能會(huì)識(shí)別到并不包含接受狀態(tài)的符號(hào),可能指針需要回退一步或者多步
| 詞法單元 | 模式 |
|---|---|
| digit | [0-9] |
| digits | digit+ |
| number | digits(.digits)?(E[+-]?digits)? |
??? 23狀態(tài)對(duì)應(yīng)的是識(shí)別為整數(shù),24狀態(tài)代表為非科學(xué)計(jì)數(shù)法的浮點(diǎn)數(shù),22匹配的是科學(xué)計(jì)數(shù)法的浮點(diǎn)數(shù)(包含整數(shù)和小數(shù)部分),同時(shí)也有只有整數(shù)部分的。狀態(tài)機(jī)會(huì)通過一個(gè)state參數(shù)來保存識(shí)別出來的編號(hào)(例如:0-24),然后通過switch 來判斷state的值,實(shí)現(xiàn)不同狀態(tài)對(duì)應(yīng)的執(zhí)行動(dòng)作。
狀態(tài)機(jī)是一個(gè)很有用的設(shè)計(jì)思想,在很多場景都有使用,大家可以下來好好學(xué)習(xí)下,React的state,Redux的狀態(tài)管理等等。

在線demo
Esprima: Parser[4] 在線demo,僅做參考,v8的解析的比這個(gè)復(fù)雜:https://v8.dev/blog/scanner [5]
語法分析
????語法分析是指根據(jù)某種給定的形式文法對(duì)由單詞序列構(gòu)成的輸入文本,例如上個(gè)階段的詞法分析產(chǎn)物-tokens stream,進(jìn)行分析并確定其語法結(jié)構(gòu)的過程。
????抽象語法樹(Abstract Syntax Tree) 是源代碼結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
????推薦這2個(gè)網(wǎng)站可以用來分析代碼生成的ast結(jié)構(gòu):Esprima: Parser[6]
https://resources.jointjs.com/demos/javascript-ast [7]
以下面代碼為例,簡單分析下對(duì)應(yīng)生成的AST樹形結(jié)構(gòu):
function?f(a,?b)?{
let?result?=?0;
if(a?>?0)?{
result?=?a?+?b;
}?else?{
result?=?a?-?b;
}
return?result
}


V8會(huì)將語法分析的過程分為兩個(gè)階段來執(zhí)行:
Pre-parser
跳過還未使用的代碼 不會(huì)生成對(duì)應(yīng)的ast,會(huì)產(chǎn)生不帶有變量的引用和聲明的scopes信息 解析速度會(huì)是Full-parser的2倍 根據(jù)js的語法規(guī)則僅拋出一些特定的錯(cuò)誤信息
Full-parser
解析那些使用的代碼 生成對(duì)應(yīng)的ast 產(chǎn)生具體的scopes信息,帶有變量引用和聲明等信息 拋出所有的js語法錯(cuò)誤
默認(rèn)函數(shù)聲明后,沒有調(diào)用,對(duì)應(yīng)的pre-parser:
??Desktop?d8?test.js?--print-ast
[generating?bytecode?for?function:?]
---?AST?---
FUNC?at?0
.?KIND?0
.?LITERAL?ID?0
.?SUSPEND?COUNT?0
.?NAME?""
.?INFERRED?NAME?""
.?DECLS
.?.?FUNCTION?"f"?=?function?f
在文件末尾增加f(1, 2)調(diào)用函數(shù)方法,就可以觸發(fā)Full-parser過程:
[generating?bytecode?for?function:?f]
---?AST?---
FUNC?at?10
.?KIND?0
.?LITERAL?ID?1
.?SUSPEND?COUNT?0
.?NAME?"f"
.?PARAMS
.?.?VAR?(0x7f80dd00fad8)?(mode?=?VAR,?assigned?=?false)?"a"
.?.?VAR?(0x7f80dd00fb80)?(mode?=?VAR,?assigned?=?false)?"b"
.?DECLS
.?.?VARIABLE?(0x7f80dd00fad8)?(mode?=?VAR,?assigned?=?false)?"a"
.?.?VARIABLE?(0x7f80dd00fb80)?(mode?=?VAR,?assigned?=?false)?"b"
.?.?VARIABLE?(0x7f80dd00fc28)?(mode?=?LET,?assigned?=?true)?"result"
.?BLOCK?NOCOMPLETIONS?at?-1
.?.?EXPRESSION?STATEMENT?at?25
.?.?.?INIT?at?25
.?.?.?.?VAR?PROXY?local[0]?(0x7f80dd00fc28)?(mode?=?LET,?assigned?=?true)?"result"
.?.?.?.?LITERAL?undefined
.?IF?at?35
.?.?CONDITION?at?40
.?.?.?GT?at?40
.?.?.?.?VAR?PROXY?parameter[0]?(0x7f80dd00fad8)?(mode?=?VAR,?assigned?=?false)?"a"
.?.?.?.?LITERAL?0
.?.?THEN?at?-1
.?.?.?BLOCK?at?-1
.?.?.?.?EXPRESSION?STATEMENT?at?51
.?.?.?.?.?ASSIGN?at?58
.?.?.?.?.?.?VAR?PROXY?local[0]?(0x7f80dd00fc28)?(mode?=?LET,?assigned?=?true)?"result"
.?.?.?.?.?.?ADD?at?63
.?.?.?.?.?.?.?VAR?PROXY?parameter[0]?(0x7f80dd00fad8)?(mode?=?VAR,?assigned?=?false)?"a"
.?.?.?.?.?.?.?VAR?PROXY?parameter[1]?(0x7f80dd00fb80)?(mode?=?VAR,?assigned?=?false)?"b"
.?.?ELSE?at?-1
.?.?.?BLOCK?at?-1
.?.?.?.?EXPRESSION?STATEMENT?at?83
.?.?.?.?.?ASSIGN?at?90
.?.?.?.?.?.?VAR?PROXY?local[0]?(0x7f80dd00fc28)?(mode?=?LET,?assigned?=?true)?"result"
.?.?.?.?.?.?SUB?at?94
.?.?.?.?.?.?.?VAR?PROXY?parameter[0]?(0x7f80dd00fad8)?(mode?=?VAR,?assigned?=?false)?"a"
.?.?.?.?.?.?.?VAR?PROXY?parameter[1]?(0x7f80dd00fb80)?(mode?=?VAR,?assigned?=?false)?"b"
.?RETURN?at?105
.?.?VAR?PROXY?local[0]?(0x7f80dd00fc28)?(mode?=?LET,?assigned?=?true)?"result"
為什么要做兩次解析?
????如果僅有一次,那必須是Full-parser,但這樣的話,大量未使用的代碼會(huì)消耗非常多的解析時(shí)間,結(jié)合具體的項(xiàng)目來看下:通過Devtools-Coverage錄制的方式可以分析頁面哪些代碼沒有用到:
????兩次解析的負(fù)面影響:如果部分代碼片段已經(jīng)被pre-parser過了,那么在執(zhí)行的過程中還會(huì)經(jīng)過一次Full-parser,那總體耗時(shí)就是0.5*parser + 1 * parser = 1.5parser。
下面羅列了不同種情況的代碼聲明對(duì)應(yīng)的解析方法:
let?a?=?0;?*//?Top-Level?頂層的代碼都是?Full-Parsing*
*//?立即執(zhí)行函數(shù)表達(dá)式?IIFE?=?Immediately?Invoked?Function?Expression*
(function?eager()?{...})();?*//?函數(shù)體是?Full-Parsing*
*//?頂層的函數(shù)非IIFE*
function?lazy()?{...}?*//?函數(shù)體是?Pre-Parsing*
lazy();?*//?-> Full-Parsing 開始解析和編譯!*
*//?強(qiáng)制觸發(fā)Full-Parsing解析*
!function?eager2()?{...},?function?eager3()?{...}?*//?full?解析*
let?f1?=?function?lazy()?{?...?};?*//?函數(shù)體是?Pre-Parsing*
let?f2?=?function?lazy()?{...}();?*//?先觸發(fā)了pre?解析,?然后又full解析*
Bad case: 深度內(nèi)嵌定義方法
function?lazy_outer()?{?*//?pre-parser*
function?inner()?{
function?inner2()?{
*//?...*
}
}
}
lazy_outer();?*//?pre-parsing?inner?&?inner2*
inner();?*//?pre-parsing?inner?&?inner2?(3rd?time!)*
Parser性能優(yōu)化Tips
使用Devtools-Coverage工具分析網(wǎng)頁無用代碼,并進(jìn)行裁剪 V8會(huì)將Parser過程的結(jié)果緩存72個(gè)小時(shí),當(dāng)用到的腳本文件中有任何代碼修改了,那么parser緩存的結(jié)果就失效了,所以好的實(shí)踐是將經(jīng)常變動(dòng)的代碼打包一起,非經(jīng)常變動(dòng)的代碼打包一起,這就有了第三方庫dll的方案。 可以考慮懶加載方案,當(dāng)有一些代碼無需業(yè)務(wù)代碼執(zhí)行的最開始需要的話,可以用異步加載的方法后續(xù)再加載
Interpret(解釋)

????解釋階段會(huì)將之前生成的AST轉(zhuǎn)換成字節(jié)碼,代碼會(huì)被編譯器編譯成從未被優(yōu)化過的機(jī)器碼,在運(yùn)行的過程中,會(huì)將需要優(yōu)化的代碼進(jìn)行熱點(diǎn)標(biāo)記,再通過更高級(jí)的編譯器進(jìn)行優(yōu)化后再編譯。
????增加字節(jié)碼的好處是,并不是將AST直接翻譯成機(jī)器碼,因?yàn)閷?duì)應(yīng)的cpu系統(tǒng)會(huì)不一致,翻譯成機(jī)器碼時(shí)要結(jié)合每種cpu底層的指令集,這樣實(shí)現(xiàn)起來代碼復(fù)雜度會(huì)非常高。
內(nèi)存占用
????最開始V8的設(shè)計(jì)中,運(yùn)行js代碼后會(huì)將AST直接轉(zhuǎn)換成二進(jìn)制的機(jī)器碼,由于執(zhí)行機(jī)器碼的效率是非常高效的,所以這種方式在發(fā)布后的一段時(shí)間內(nèi)運(yùn)行效果是非常好。
????但機(jī)器碼會(huì)存儲(chǔ)在內(nèi)存中,退出進(jìn)程后會(huì)存儲(chǔ)在磁盤上,但如果js源碼可能只有1M,但轉(zhuǎn)換后的機(jī)器碼可能會(huì)多達(dá)幾十M,過度占用會(huì)導(dǎo)致性能大大降低。當(dāng)手機(jī)越來變得普及后,內(nèi)存問題就更加突出了。
Ignition解釋器
????V8團(tuán)隊(duì)為了解決這類性能問題,自己做出了Ignition[8]解釋器,在中間過程中增加了字節(jié)碼
????Ignition解釋器轉(zhuǎn)換成的字節(jié)碼,比傳統(tǒng)的直接翻譯成機(jī)器碼節(jié)省了 25%-50% 的內(nèi)存空間,同時(shí)為了進(jìn)一步節(jié)省,當(dāng)字節(jié)碼生成后,AST的數(shù)據(jù)就直接被廢棄掉了。在字節(jié)碼上又加上了一些元數(shù)據(jù),例如記錄源代碼的位置和用于執(zhí)行字節(jié)碼的處理方法等。
還是以上面的代碼為例,看下d8下的字節(jié)碼
function?add(x,?y)?{
var?z?=?x?+?y;
return?z;
}
add(1,2);
? Desktop d8 test.js --print-bytecode
[generated bytecode for function: (0x3ae908292f71 )]
Bytecode length: 28
Parameter count 1
Register count 4
Frame size 32
OSR nesting level: 0
Bytecode Age: 0
0x3ae908293032 @ 0 : 13 00 LdaConstant [0]
0x3ae908293034 @ 2 : c2 Star1
0x3ae908293035 @ 3 : 19 fe f8 Mov , r2
0x3ae908293038 @ 6 : 64 4f 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
0x3ae90829303d @ 11 : 21 01 00 LdaGlobal [1], [0]
0x3ae908293040 @ 14 : c2 Star1
0x3ae908293041 @ 15 : 0d 03 LdaSmi [3]
0x3ae908293043 @ 17 : c1 Star2
0x3ae908293044 @ 18 : 0d 04 LdaSmi [4]
0x3ae908293046 @ 20 : c0 Star3
0x3ae908293047 @ 21 : 62 f9 f8 f7 02 CallUndefinedReceiver2 r1, r2, r3, [2]
0x3ae90829304c @ 26 : c3 Star0
0x3ae90829304d @ 27 : a8 Return
Constant pool (size = 2)
0x3ae908293001: [FixedArray] in OldSpace
- map: 0x3ae908002205 ????V8在執(zhí)行字節(jié)碼的過程中,使用到了通用寄存器和累加寄存器,函數(shù)參數(shù)和局部變量保存在通用寄存器里面,累加器中保存中間計(jì)算結(jié)果,在執(zhí)行指令的過程中,如果直接由cpu從內(nèi)存中讀取數(shù)據(jù)的話,比較影響程序執(zhí)行的性能,使用寄存器存儲(chǔ)中間數(shù)據(jù)的設(shè)計(jì),可以大大提升cpu執(zhí)行的速度。
????這里面包含了很多編譯原理里面涉及到的指令集。
????字節(jié)碼更多指令可以看下V8-Ignition的源碼[9]
參考文檔:Google Ignition ppt[10]
編譯器
????這個(gè)過程主要指是V8的TurboFan編譯器將字節(jié)碼翻譯成機(jī)器碼的過程。
字節(jié)碼配合解釋器和編譯器這一技術(shù)設(shè)計(jì),可以稱為JIT,即時(shí)編譯技術(shù),java虛擬機(jī)也是類似的技術(shù),解釋器在解釋執(zhí)行字節(jié)碼時(shí),會(huì)收集代碼信息,標(biāo)記一些熱點(diǎn)代碼,熱點(diǎn)代碼(hotspot)就是一段代碼被重復(fù)執(zhí)行多次,TurboFan會(huì)將熱點(diǎn)代碼直接編譯成機(jī)器碼,緩存起來,下次調(diào)用直接運(yùn)行對(duì)應(yīng)的二進(jìn)制的機(jī)器碼,加速執(zhí)行速度。
TurboFan的整體優(yōu)化過程,可參見下圖,這里的優(yōu)化分為了3層,更偏向于系統(tǒng)底層 
在TurboFan將字節(jié)碼編譯成機(jī)器碼的過程中,還進(jìn)行了簡化處:常量合并、強(qiáng)制折減、代數(shù)重新組合。
類型推斷(Speculative Optimization)是TurboFan的一大核心能力,
Execution(執(zhí)行)
????在Javascript的執(zhí)行過程中,經(jīng)常遇到的就是對(duì)象屬性的訪問。作為一種動(dòng)態(tài)的語言,在js中,一行簡單的屬性訪問可能包含著復(fù)雜的語義: Object.xxx的形式,可能是屬性的直接訪問,也可能調(diào)用的對(duì)象的Getter方法,還有可能是要通過原型鏈往上層對(duì)象中查找。
????這種不確定性而且動(dòng)態(tài)判斷的情況,會(huì)浪費(fèi)很多查找時(shí)間,降低運(yùn)行的速度,V8中會(huì)把第一次分析的結(jié)果放在緩存中,當(dāng)再次訪問相同的屬性時(shí),會(huì)優(yōu)先從緩存中去取,調(diào)用GetProperty(Object, "xxx", feedback_cache)的方法獲取緩存,如果有緩存結(jié)果,就會(huì)跳過查找過程。
Object Shapes
????在靜態(tài)語言中,代碼執(zhí)行前要先進(jìn)行編譯,編譯的時(shí)候,每個(gè)對(duì)象的屬性都是固定的,
直接可以通過記錄某個(gè)屬性相對(duì)該對(duì)象的地址的偏移量,就可直接讀取到屬性值,
而在動(dòng)態(tài)語言中,對(duì)象的屬性是會(huì)被實(shí)時(shí)改動(dòng)的,能否可以借鑒靜態(tài)語言的這種特點(diǎn)來設(shè)計(jì)呢?
????V8加入了Object Shapes 或者叫做Hidden Class(隱藏類)的概念。V8會(huì)給每個(gè)對(duì)象創(chuàng)建一個(gè)隱藏類,里面記錄了對(duì)象的一些基本信息:
對(duì)象所包含的所有屬性 每個(gè)屬性相對(duì)于該對(duì)象的偏移量
????有了屬性名和地址的偏移量,當(dāng)訪問對(duì)象的某個(gè)屬性時(shí),就可以直接從內(nèi)存中讀取到,不需要再經(jīng)過一系列的查找,大大提升了V8訪問對(duì)象時(shí)的效率。以代碼為例:
let?demoObj?=?{?a:1,?b:2?};?%DebugPrint(demoObj);?//?d8內(nèi)部api?
執(zhí)行d8的調(diào)試命令查看對(duì)應(yīng)的隱藏類結(jié)構(gòu):
??Desktop?d8?--allow-natives-syntax?test2.js
DebugPrint:?0x2ef2081094b5:?[JS_OBJECT_TYPE]
-?map:?0x2ef2082c78c1?<Map(HOLEY_ELEMENTS)>?[FastProperties]?//隱藏類地址
-?prototype:?0x2ef208284205?<Object?map?=?0x2ef2082c21b9>?//?原型鏈
-?elements:?0x2ef20800222d?0]>?[HOLEY_ELEMENTS]?//?elements?和?properties?快慢屬性相關(guān)
-?properties:?0x2ef20800222d?0]>
-?All?own?properties?(excluding?elements):?{
0x2ef20808ecf9:?[String]?in?ReadOnlySpace:?#a:?1?(const?data?field?0),?location:?in-object
0x2ef20808ed95:?[String]?in?ReadOnlySpace:?#b:?2?(const?data?field?1),?location:?in-object
}
0x2ef2082c78c1:?[Map]?//?對(duì)應(yīng)的隱藏類
-?type:?JS_OBJECT_TYPE
-?instance?size:?20
-?inobject?properties:?2
-?elements?kind:?HOLEY_ELEMENTS
-?unused?property?fields:?0
-?enum?length:?invalid
-?stable_map
-?back?pointer:?0x2ef2082c7899?<Map(HOLEY_ELEMENTS)>
-?prototype_validity?cell:?0x2ef208202405?1> -?instance?descriptors?(own)?#2:?0x2ef2081094e5?2]> -?prototype:?0x2ef208284205?<Object?map?=?0x2ef2082c21b9> -?constructor:?0x2ef208283e3d? -?dependent?code:?0x2ef2080021b9? -?construction?counter:?0
| 隱藏類的復(fù)用
????現(xiàn)在我們清楚每個(gè)對(duì)象都有一個(gè)map屬性,指向的是一個(gè)隱藏類,如果兩個(gè)相同形狀的對(duì)象,在V8中會(huì)復(fù)用同一個(gè)隱藏類,這樣會(huì)減少創(chuàng)建隱藏類的次數(shù),加快V8的執(zhí)行速度,同時(shí)也會(huì)減少隱藏類占用的內(nèi)存空間,相同形狀的定義:
相同的屬性名稱 相同的屬性名順序一致 相同的屬性個(gè)數(shù)
let?demoObj?=?{?a:1,?b:2?};?
let?demoObj2?=?{?a:100,?b:200?};?
%DebugPrint(demoObj);?
%DebugPrint(demoObj2);?

????當(dāng)對(duì)象的隱藏類創(chuàng)建完成后,一旦對(duì)象發(fā)生形狀上的改變:增加新的屬性或者是刪除舊的屬性時(shí),隱藏類就會(huì)重新被創(chuàng)建,這個(gè)動(dòng)作是V8執(zhí)行過程中的一筆開銷。所以這里的代碼優(yōu)化建議:
盡量創(chuàng)建形狀一致的對(duì)象,屬性的順序、屬性的key值、key值的個(gè)數(shù)盡量保持一致 盡量不要后面再加入臨時(shí)屬性,聲明對(duì)象時(shí)屬性完整,因?yàn)槊看渭尤雽傩?,破壞了原有的?duì)象形狀,隱藏類要重新創(chuàng)建 盡量不要使用delete 刪除對(duì)象屬性,同理于上條原理,會(huì)破壞對(duì)象的形狀
總結(jié)
? ? 本次分享主要是從js腳本內(nèi)容下載到最終在V8引擎執(zhí)行的過程進(jìn)行分析,來體會(huì)一下V8的獨(dú)到之處,和它不斷追求極致性能的思想。加深對(duì)V8的理解,能更好地寫出高效的代碼,歡迎一起探究v8引擎背后絕妙的設(shè)計(jì)思想。
參考文檔
瀏覽器工作原理-webkit內(nèi)核研究[11]
Using d8 · V8[12]
V8 JavaScript engine[13] 官網(wǎng)
認(rèn)識(shí) V8 引擎[14]
[譯] Blink內(nèi)核是如何工作的?[15]
詞法分析--手動(dòng)詞法單元的識(shí)別(狀態(tài)轉(zhuǎn)換圖、KMP算法)[16]
v8 parser JS[17]
利用 V8 深入理解 JavaScript 設(shè)計(jì)[18]
[譯] V8引擎中基于推測(cè)的優(yōu)化介紹[19]
JS引擎工作原理詳解[20]

