Netscape Navigator 在 90 在年代中期對(duì) JavaScript 進(jìn)行了集成,這讓網(wǎng)頁(yè)開(kāi)發(fā)人員對(duì) HTML 頁(yè)面中諸如 form 、frame 和 image 之類的元素的訪問(wèn)變得非常容易。由此 JavaScript 很快成為了用于定制控件和添加動(dòng)畫的工具,到 90 年代后期的時(shí)候,大部分的 JavaScript 腳本僅僅完成像“根據(jù)用戶的鼠標(biāo)動(dòng)作把一幅圖換成另一幅圖”這樣簡(jiǎn)單的功能。
隨著最近 AJAX 技術(shù)的興起,JavaScript 現(xiàn)在已經(jīng)變成了實(shí)現(xiàn)基于 web 的應(yīng)用程序(例如我們自己的 Gmail)的核心技術(shù)。JavaScript 程序從聊聊幾行變成數(shù)百 KB 的代碼。JavaScript 被設(shè)計(jì)于完成一些特定的任務(wù),雖然 JavaScript 在做這些事情的時(shí)候通常都很高效,但是性能已經(jīng)逐漸成為進(jìn)一步用 JavaScript 開(kāi)發(fā)復(fù)雜的基于 web 的應(yīng)用程序的瓶頸。
V8 是一個(gè)全新的 JavaScript 引擎,它在設(shè)計(jì)之初就以高效地執(zhí)行大型的 JavaScript 應(yīng)用程序?yàn)槟康?。在一?a target="_blank" rel="nofollow">性能測(cè)試中,V8 比 Internet Explorer 的 JScript 、Firefox 中的 SpiderMonkey 以及 Safari 中的 JavaScriptCore 要快上數(shù)倍。如果你的 web 程序的瓶頸在于 JavaScript 的運(yùn)行效率,用 V8 代替你現(xiàn)在的 JavaScript 引擎很可能可以提升你的程序的運(yùn)行效率。具體會(huì)有多大的性能提升依賴于程序執(zhí)行了多少 JavaScript 代碼以及這些代碼本身的性質(zhì)。比如,如果你的程序中的函數(shù)會(huì)被反復(fù)執(zhí)行很多遍的話,性能提升通常會(huì)比較大,反過(guò)來(lái),如果代碼中有很多不同的函數(shù)并且都只會(huì)被調(diào)用一次左右,那么性能提升就不會(huì)那么明顯了。其中的原因在你讀過(guò)這份文檔余下的部分之后就會(huì)明白了。
V8 的性能提升主要來(lái)自三個(gè)關(guān)鍵部分:
快速屬性訪問(wèn)
動(dòng)態(tài)機(jī)器碼生成
高效的垃圾收集
快速屬性訪問(wèn)
JavaScript 是一門動(dòng)態(tài)語(yǔ)言,屬性可以在運(yùn)行時(shí)添加到或從對(duì)象中刪除。這意味著對(duì)象的屬性經(jīng)常會(huì)發(fā)生變化。大部分 JavaScript 引擎都使用一個(gè)類似于字典的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)對(duì)象的屬性,這樣每次訪問(wèn)對(duì)象的屬性都需要進(jìn)行一次動(dòng)態(tài)的字典查找來(lái)獲取屬性在內(nèi)存中的位置。這種實(shí)現(xiàn)方式讓 JavaScript 中屬性的訪問(wèn)比諸如 Java 和 Smalltalk 這樣的語(yǔ)言中的成員變量的訪問(wèn)慢了許多。成員變量在內(nèi)存中的位置離對(duì)象的地址的距離是固定的,這個(gè)偏移量由編譯器在編譯的時(shí)候根據(jù)對(duì)象的類的定義決定下來(lái)。因此對(duì)成員變量的訪問(wèn)只是一個(gè)簡(jiǎn)單的內(nèi)存讀取或?qū)懭氲牟僮?,通常只需要一條指令即可。
為了減少 JavaScript 中訪問(wèn)屬性所花的時(shí)間,V8 采用了和動(dòng)態(tài)查找完全不同的技術(shù)來(lái)實(shí)現(xiàn)屬性的訪問(wèn):動(dòng)態(tài)地為對(duì)象創(chuàng)建隱藏類。這并不是什么新的想法,基于原型的編程語(yǔ)言 Self 就用 map 來(lái)實(shí)現(xiàn)了類似的功能(參見(jiàn) An Efficient Implementation of Self, a Dynamically-Typed Object-Oriented Language Based on Prototypes )。在 V8 里,當(dāng)一個(gè)新的屬性被添加到對(duì)象中時(shí),對(duì)象所對(duì)應(yīng)的隱藏類會(huì)隨之改變。
下面我們用一個(gè)簡(jiǎn)單的 JavaScript 函數(shù)來(lái)加以說(shuō)明:
function Point(x, y) {
this.x = x;
this.y = y;
}
當(dāng) new Point(x, y)執(zhí)行的時(shí)候,一個(gè)新的 Point對(duì)象會(huì)被創(chuàng)建出來(lái)。如果這是 Point對(duì)象第一次被創(chuàng)建,V8 會(huì)為它初始化一個(gè)隱藏類,不妨稱作C0。因?yàn)檫@個(gè)對(duì)象還沒(méi)有定義任何屬性,所以這個(gè)初始類是一個(gè)空類。到這個(gè)時(shí)候?yàn)橹?,?duì)象Point的隱藏類是 C0。

執(zhí)行函數(shù) Point 中的第一條語(yǔ)句(this.x = x;)會(huì)為對(duì)象 Point 創(chuàng)建一個(gè)新的屬性 x。此時(shí),V8 會(huì):
- 在 C0 的基礎(chǔ)上創(chuàng)建另一個(gè)隱藏類 C1,并將屬性 x 的信息添加到 C1 中:這個(gè)屬性的值會(huì)被存儲(chǔ)在距 Point 對(duì)象的偏移量為 0 的地方。
- 在 C0 中添加適當(dāng)?shù)念愞D(zhuǎn)移信息,使得當(dāng)有另外的以其為隱藏類的對(duì)象在添加了屬性 x之后能夠找到 C1 作為新的隱藏類。此時(shí)對(duì)象 Point 的隱藏類被更新為 C1。

執(zhí)行函數(shù) Point 中的第二條語(yǔ)句(this.y = y;)會(huì)添加一個(gè)新的屬性 y 到對(duì)象 Point中。同理,此時(shí) V8 會(huì):
- 在 C1 的基礎(chǔ)上創(chuàng)建另一個(gè)隱藏類 C2,并在 C2 中添加關(guān)于屬性 y 的信息:這個(gè)屬性將被存儲(chǔ)在內(nèi)存中離 Point 對(duì)象的偏移量為 1 的地方。
- 在 C1 中添加適當(dāng)?shù)念愞D(zhuǎn)移信息,使得當(dāng)有另外的以其為隱藏類的對(duì)象在添加了屬性 y之后能夠找到 C2 作為新的隱藏類。此時(shí)對(duì)象 Point 的隱藏類被更新為 C2。

咋一看似乎每次添加一個(gè)屬性都創(chuàng)建一個(gè)新的隱藏類非常低效。實(shí)際上,利用類轉(zhuǎn)移信息,隱藏類可以被重用。下次創(chuàng)建一個(gè) Point 對(duì)象的時(shí)候,就可以直接共享由最初那個(gè) Point 對(duì)象所創(chuàng)建出來(lái)的隱藏類。例如,如果又一個(gè) Point 對(duì)象被創(chuàng)建出來(lái)了:
- 一開(kāi)始 Point 對(duì)象沒(méi)有任何屬性,它的隱藏類將會(huì)被設(shè)置為 C0。
- 當(dāng)屬性 x 被添加到對(duì)象中的時(shí)候,V8 通過(guò) C0 到 C1 的類轉(zhuǎn)移信息將對(duì)象的隱藏類更新為 C1 ,并直接將 x 的屬性值寫入到由 C1 所指定的位置(偏移量 0)。
- 當(dāng)屬性 y 被添加到對(duì)象中的時(shí)候,V8 又通過(guò) C1 到 C2 的類轉(zhuǎn)移信息將對(duì)象的隱藏類更新為 C2 ,并直接將 y 的屬性值寫入到由 C2 所指定的位置(偏移量 1)。
盡管 JavaScript 比通常的面向?qū)ο蟮木幊陶Z(yǔ)言都要更加動(dòng)態(tài)一些,然而大部分的 JavaScript 程序都會(huì)表現(xiàn)出像上述描述的那樣的運(yùn)行時(shí)高度結(jié)構(gòu)重用的行為特征來(lái)。使用隱藏類主要有兩個(gè)好處:屬性訪問(wèn)不再需要?jiǎng)討B(tài)字典查找了;為 V8 使用經(jīng)典的基于類的優(yōu)化和內(nèi)聯(lián)緩存技術(shù)創(chuàng)造了條件。關(guān)于內(nèi)聯(lián)緩存的更多信息可以參考 Efficient Implementation of the Smalltalk-80 System 這篇論文。
動(dòng)態(tài)機(jī)器碼生成V8
在第一次執(zhí)行 JavaScript 代碼的時(shí)候會(huì)將其直接編譯為本地機(jī)器碼,而不是使用中間字節(jié)碼的形式,因此也沒(méi)有解釋器的存在。屬性訪問(wèn)由內(nèi)聯(lián)緩存代碼來(lái)完成,這些代碼通常會(huì)在運(yùn)行時(shí)由 V8 修改為合適的機(jī)器指令。
在第一次執(zhí)行到訪問(wèn)某個(gè)對(duì)象的屬性的代碼時(shí),V8 會(huì)找出對(duì)象當(dāng)前的隱藏類。同時(shí),V8 會(huì)假設(shè)在相同代碼段里的其他所有對(duì)象的屬性訪問(wèn)都由這個(gè)隱藏類進(jìn)行描述,并修改相應(yīng)的內(nèi)聯(lián)代碼讓他們直接使用這個(gè)隱藏類。當(dāng) V8 預(yù)測(cè)正確的時(shí)候,屬性值的存取僅需一條指令即可完成。如果預(yù)測(cè)失敗了,V8 會(huì)再次修改內(nèi)聯(lián)代碼并移除剛才加入的內(nèi)聯(lián)優(yōu)化。
例如,訪問(wèn)一個(gè) Point 對(duì)象的 x 屬性的代碼如下:
point.x
在 V8 中,對(duì)應(yīng)生成的機(jī)器碼如下:
# ebx = the point object
cmp [ebx,<hidden class offset>],<cached hidden class>
jne <inline cache miss>
mov eax,[ebx, <cached x offset>]
如果對(duì)象的隱藏類和緩存的隱藏類不一樣,執(zhí)行會(huì)跳轉(zhuǎn)到 V8 運(yùn)行系統(tǒng)中處理內(nèi)聯(lián)緩存預(yù)測(cè)失敗的地方,在那里原來(lái)的內(nèi)聯(lián)代碼會(huì)被修改以移除相應(yīng)的內(nèi)聯(lián)緩存優(yōu)化。如果預(yù)測(cè)成功了,屬性 x 的值會(huì)被直接讀出來(lái)。
當(dāng)有許多對(duì)象共享同一個(gè)隱藏類的時(shí)候,這樣的實(shí)現(xiàn)方式下屬性的訪問(wèn)速度可以接近大多數(shù)動(dòng)態(tài)語(yǔ)言。使用內(nèi)聯(lián)緩存代碼和隱藏類實(shí)現(xiàn)屬性訪問(wèn)的方式和動(dòng)態(tài)代碼生成和優(yōu)化的方式結(jié)合起來(lái),讓大部分 JavaScript 代碼的運(yùn)行效率得以大幅提升。
高效的垃圾收集
V8 會(huì)自動(dòng)回收不再被對(duì)象使用的內(nèi)存,這個(gè)過(guò)程通常被稱為“垃圾收集(Garbage Collection)”。為了保證快速的對(duì)象分配和縮短由垃圾收集造成的停頓,并杜絕內(nèi)存碎片,V8 使用了一個(gè) stop-the-world, generational, accurate 的垃圾收集器,換句話說(shuō),V8 的垃圾收集器:
- 在執(zhí)行垃圾回收的時(shí)候會(huì)中斷程序的執(zhí)行。
- 大部分情況下,每個(gè)垃圾收集周期只處理整個(gè)對(duì)象堆的一部分,這讓程序中斷造成的影響得以減輕。
- 總是知道內(nèi)存中所有的對(duì)象和指針?biāo)诘奈恢茫@避免了非 accurate 的垃圾收集器中普遍存在的由于錯(cuò)誤地把對(duì)象當(dāng)作指針而造成的內(nèi)存溢出的情況。
在 V8 中,對(duì)象堆被分成兩部分:用于為新創(chuàng)建的對(duì)象分配空間的部分和用于存放在垃圾收集周期中生存下來(lái)的那些老的對(duì)象的部分。如果一個(gè)對(duì)象在垃圾收集的過(guò)程中被移動(dòng)了,V8 會(huì)更新所有指向這個(gè)對(duì)象的指針到新的地址。
本文翻譯自https://developers.google.com/v8/design?csw=1
文章轉(zhuǎn)載自http://blog.pluskid.org/?p=186