Google研發的V8 JavaScript引擎性能優異。咱們請熟悉內部程序實現的做者依源代碼來看看V8是如何加速的。 javascript
做者:Community Engine公司研發部研發工程師Hajime Morita java
Google的Chrome中的V8 JavaScript引擎,因爲性能良好吸引了至關的注目。它是Google特別爲了Chrome能夠高速運行網頁應用(WebApp)而開發的。Chrome利用Apple領導的WebKit研發計劃做爲渲染引擎(Rendering engine)。 WebKit也被用在Safari瀏覽器中。WebKit的標準配備有稱爲JavaScriptCore的JavaScript引擎,但Chrome則以V8取代之(圖1)。 程序員
V8開發小組是一羣程序語言專家。核心工程師Lars Bak以前研發了HotSpot,這是用在Sun Microsystems公司開發的Java虛擬機器(VM)之加速技術。他也在美國的Animorphic Systems公司(於1997年被Sun Microsystems所併購)研發了稱爲Strongtalk的實驗Smalltalk系統。V8充分發揮了研發HotSpot和Strongtalk時所得到的知識。 算法
圖1 開發本身的JavaScript引擎 Apple的Safari和Google的Chrome使用相同的渲染引擎。配有JavaScriptCore的WebKit渲染引擎在JavaScript引擎中是標準配備,但在Chrome卻被V8取代了. shell
高速引擎的需求 數組
Google研發小組在2006年開始研發V8,部分的緣由是Google對既有JavaScript引擎的執行速度不滿意。我認爲當時JavaScript引擎很慢是有兩個緣由的:開發的歷史背景,以及JavaScript語言的複雜性。 瀏覽器
JavaScript存在至少10年了。在1995年,它出如今網景(Netscape Communications)公司所研發的網頁瀏覽器Netscape Navigator 2.0中。然而有段時間人們對於性能的要求不高,由於它只用在網頁上少數的動畫、交互操做或其它相似的動做上。(最明確的是爲了減小網絡傳輸,以提升效率和改善交互性!)瀏覽器的顯示速度視網絡傳輸速度以及渲染引擎(rendering engine)解析HTML、風格樣式表(cascading style sheets, CSS)及其餘代碼的速度而定。瀏覽器的開發工做優先提高渲染引擎的速度,而JavaScript的處理速度不是過重要。同時出現的Java有至關大的進步,它被作得越來越快,以便和C++競爭。 緩存
然而,在過去幾年,JavaScript忽然受到普遍使用。緣由是以前被當成桌面應用的軟件(其中包括Office套件等),現已成爲能夠在瀏覽器中執行的軟件。 服務器
Google自己就推出了好幾款JavaScript網絡應用,其中包括它的Gmail電子郵件服務、Google Maps地圖數據服務、以及Google Docs office套件。 網絡
這些應用表現出的速度不只受到服務器、網絡、渲染引擎以及其餘諸多因素的影響,同時也受到JavaScript自己執行速度的影響。然而既有的JavaScript引擎沒法知足新的需求,而性能不佳一直是網絡應用開發者最關心的。
語言自己的問題
JavaScript語言的規範如今性能壓力巨大。例如,這在當它斷定變量類型時就至關顯而易見。如C++和Java等主流語言採用靜態類型(static typing)。當代碼編譯時,就可宣告變量類型。因爲不須要在執行期間檢查數據類型,所以靜態類型佔有性能上的優點。
在例如C++和Java等通常處理系統中,fields*和methods*等的內容是以數組儲存,以1:1位移(offset)對應fields和methods等的名稱(圖2)。個別變量和methods等儲存的位置,是針對各個類定義的。在C++和Java等語言中,已事先知道所存取的變量(類)類型,因此語言解釋系統(Interpreting system)只要利用數組和位移來存取field和method等。位移使它只要幾個機器語言指令,就能夠存取field、找出field或執行其餘任務。
圖2 JavaScript和C++、Java的不一樣 C++、Java及其餘處理系統將fields和methods等,以它們的名稱以1:1對應數組內的位移值儲存在數組中。會事先知道要存取的變量類型(類),所以能夠只用數組和位移就能夠存取fields和methods等。然而在JavaScript,個別的對象都有本身屬性和方法等的表格。每一次程序存取屬性或是呼叫方法時,都必須檢查對象的類型並執行適當的處理。
* Field:屬對象的變量。C++中稱爲成員變量。
* Method:屬對象的處理類型。C++中稱爲成員函式。
* Property屬性:JavaScript屬性是對象本身擁有的變量。在JavaScript中,屬性中不僅能夠是標準的值,也能夠是methods。
* Hash table哈希表:一種數據結構會傳回與特定關鍵相關之對應值。它有一個內部數組,使用鍵值(key)所產生之Hash值做爲數組中特定位置清單值的位移。若是恰好在相同的位置上產生不一樣關鍵之Hash值時,清單位置會儲存多個值,這意味着在傳回任何值以前必須先檢查Hash值是否符合。
而另一方面,JavaScript則是利用動態類型(dynamic typing)。 JavaScript變量沒有類型,而所指定對象的類型在第一次執行時(換言之,動態地)就已斷定了。每次在JavaScript中存取屬性(property),或是尋求方法等,必須檢查對象的類型,並照着進行處理。
許多JavaScript引擎都使用哈希表(hash table)來存取屬性和尋找方法等。換言之,每次存取屬性或是尋找方法時,就會使用字符串做爲尋找對象哈希表的鍵(key)(圖3)。
圖3 屬性存取時的內部JavaScript處理 使用對象x哈希表的字符串「foo」做爲搜尋「foo」內容的關鍵字。
搜尋哈希表是一個連續動做,包含從散列(hashing)值中斷定數組內位置,而後查看該位置的鍵值(key)是否符相等。而後可使用位移直接讀取數據的數組比較起來,利用此方法存取較費時。
使用動態類型的其餘語言,還有Smalltalk和Ruby等。這些語言基本上也是搜尋哈希表,但它們利用類來縮短搜尋時間。然而,JavaScript沒有類。除了「Numbers」指示數字值、「Strings」爲字符串以及其餘少數幾種類型外,其餘對象都是「Object」型。程序員沒法宣告類型(類),所以沒法使用明確的類型來加速處理。
JavaScript的彈性容許在任什麼時候間,在對象上新增或是刪除屬性和方法等(請參閱附錄)。JavaScript語言很是動態,而業界的通常見解是動態語言比C++或Java等靜態語言更難加速。儘管有困難,但V8利用好幾項技術來達到加速的目的:
1.JIT編譯 (JIT Compile)
不用字節碼(bytecode)生成機器語言
從性能的角度來看,V8具備4個主要特性。首先,它在執行時以稱爲及時(just-in-time, JIT)的編譯方法,來產生機器語言。這是個廣泛用來改善解釋速度的方法,在Java和.NET等語言中也能夠發現此方法。V8比Firefox中的SpiderMonkey JavaScript引擎,或Safari的JavaScriptCore等競爭引擎還要早的實踐了這一技術。
V8 JIT編譯器在產生機器語言時,不會產生中間碼(圖4)。例如,在Java編譯器先將原始碼轉換成一個以虛擬中間語言(稱爲字節碼,bytecode)表示的一類文件 (class file)。Java編譯器和字節碼編譯器產生字節碼,而非機器語言。Java VM按順序地在執行中解釋字節碼。此執行模式稱爲字節碼解釋器(bytecode interpreter)。 Firefox的SpiderMonkey具備一個內部的字節碼編譯器和字節解釋器,將JavaScript原始碼轉換成它自家特點的字節代碼,以便執行。
圖4 V8的JIT編譯器直接輸出機器語言程序語言系統先使用語法分析器將原始碼轉換成抽象語法樹(abstract syntax tree, AST)。以前有幾種方式來處理。字節碼編譯器將抽象語法樹編譯爲中間代碼,而後在編譯器中執行。如Java JIT等混合模式將這中間代碼的一部分編譯成機器語言,以改善處理性能。Chrome不使用中間代碼,JIT直接從抽象語法樹來編譯機器語言。也有抽象語法樹解釋器,直接解析抽象語法樹。
事實上,Java VM目前使用一個以HotSpot爲基礎的JIT編譯器。它扮演字節碼解釋器的角色,來解析代碼,將常執行的代碼區塊轉換成機器語言而後執行,這就是混合模式(hybrid model)。
字節碼解釋器、混合模式等等,具備製做簡單且有絕佳可移植性的優勢。只要是引擎能夠編譯的原始碼,那麼就能夠在任何CPU架構上執行字節碼,這正是爲何該技術被稱爲「虛擬機(VM)」的緣由。即便在產生機器代碼的混合模式中,能夠藉由編寫字節碼的解釋器開始進行開發,而後實現機器語言生成器。經過使用簡單的位元碼,在機器代碼產生時,要將輸出最佳化就變得容易許多。
V8不是將原始程序轉換成中間語言,而是將抽象語法直接產生機器語言並加以執行。沒有虛擬機,且由於不須要中間表示式,程序處理會更早開始了。然而,另外一方面,它也喪失了虛擬機的好處,例如透過字節碼解釋器和混合模式等,所帶來的高可移植性(portability)和優化的簡易性等。
2.垃圾回收管理
Java標準特性的精妙實現
第二個關鍵的特性是,V8將垃圾回收管理(garbage collection, GC*)實做爲「精確的GC*」。相反的,大部分的JavaScript引擎、Ruby及其餘語言編譯器都是使用保守的GC*(conservative GC),由於保守的GC實做簡單許多。雖然精確的GC更爲複雜,但也有性能上的優勢。Oracle(Sun)的Java VM就是使用精確GC。
* Garbage collection(GC)垃圾回收管理:自動偵測被程序保留但已再也不使用的存儲器空間並釋放。
* 保守(conservative) GC:沒有分別嚴格管理指標器和數字值之存儲器回收管理。此方法是若是它能夠成爲指標,那就以指標來看待它,即便它可能個數值。此方法防止對象被意外回收,但它也沒法釋出可能的存儲器。
雖然精確GC自己就是高效率的,但以精確GC爲基礎的高級算法,如分代(Generational) GC、複製(copy) GC以及標記和精簡處理(mark-and-compact processing)等在性能上有明顯的改善。分代(Generational) GC藉由分開管理「年青分代(Young Generational)」對象(常常收集)和「舊分代(Old Generational)」對象(相對長壽的對象)而提高了GC效率。
V8使用了分代(Generational)GC,在新分代(Generational)處理上使用輕度(light-load)複製GC,而在舊GC上使用標記和精簡GC,由於它須在內存空間內移動對象。這很難在保守GC中執行。在對象的複製中,壓縮(compaction)(在硬盤方面稱爲defrag)和相似動做時,對象的地址會改變,且基於這個緣由,最廣泛的方法是用「句柄」(handles)間接地引用地址。然而,V8不使用句柄(handles),而是重寫該對象引用的全部數據。不使用句柄(handles)會使實現更困難,但卻能改善性能由於少了間接引用。Java VM HotSpot也使用相同的技術。
3.內嵌緩存(inline cache)
JavaScript中不可用?
V8目前能夠針對x86和ARM架構產生適合的機器語言。雖然沒采用C++或Java中傳統的優化方式,V8仍是有動態語言與生俱來的速度。
其中一項良好範例是內嵌緩存(inline cache),這項技巧能夠避免方法呼叫和屬性存取時的哈希表搜尋。它能夠當即緩存以前的搜尋結果,所以稱爲「內嵌」。人們知道此技術已有一段時間了,已經被應用在Smalltalk、Java和Ruby等語言中。
內嵌緩存假設對象都有類型之分,但在JavaScript語言中卻沒有。直到V8出現後,而這就是爲何之前的JavaScript引擎都沒有內嵌緩存的緣由。
爲了突破此限制,V8在執行時就分析程序操做,並利用「隱藏類」(hidden classes)爲對象指定暫時的類。有了隱藏類,即便是JavaScript也可使用內嵌緩存。可是這些類是提高執行速度之技巧,不是語言規範的延伸。因此它們沒法在JavaScript代碼中引用。
4.隱藏類
儲存類型轉換信息
隱藏類爲沒有類之分的JavaScript語言規範帶來有趣的挑戰,同時也是V8用來提高速度最獨特的技巧。它們值得更深刻的探究。
在V8中創建類有兩個主要的理由,即(1)將屬性名稱相同的對象歸類,及(2)識別屬性名稱不一樣的對象。前一類中的對象有徹底相同的對象描述,而這能夠加速屬性存取。
在V8,符合歸類條件的類會配置在各類JavaScript對象上。對象引用所配置的類(圖5)。然而這些類只存在於V8做爲方便之用,因此它們是「隱藏」的。
圖5 V8對象有隱藏類的引用 若是對象的描述是相同的,那麼隱藏類也會相同。在此範例中,對象p和q都屬於相同的隱藏類
我上面提到隨時能夠在JavaScript中新增或刪除屬性。然而當此事發生時會毀壞歸類條件(概括名稱相同的屬性)。V8藉由創建屬性變化所需的新類來解決。屬性改變的對象透過一個稱爲「類型轉換(class transition)」的程序歸入新級別中。
第二個目標-識別屬性名稱不一樣的對象-則是藉由創建新類來達成。然而,若是每一次屬性改變就創建一個新類的話,那就沒法持續達到第一個目標了(概括名稱相同的屬性)。
圖6 配置新類:類型轉換屬性改變的對象會被歸爲新類。當對象p增長了新屬性z時,對象p就會被歸爲新類。
V8將變換信息儲存在類內,來解決此問題。考量圖7,它說明了圖6中所示的情形,當隱藏類Point有x和y屬性時,新屬性x就會新增至Point級的對象p中。當新屬性z加到對象p時,V8會將「新增屬性p,創建Point2類」的信息儲存在Point級的內部表格中(圖7,步驟1)。
圖7 在類中儲存類變換信息當在對象p中加入新屬性z時,V8會在Point類內的表格上記錄「加入屬性z,創建類Point2」(步驟1)。當同一Point類的對象q加入屬性z時,V8會先搜尋Point類表。若是它發現了Point2類已加入屬性z時,就會將對象q設定在Point2類(步驟2)。
當新屬性z新增至也是Point級的對象q時,V8會先搜尋Point級的表格,並發現Point2級已加入屬性z。在表格中找到類時,對象q就會被設定至該類(Point2),而不創建新類(圖7,步驟2)。這就達到了概括屬性名稱相同的對象之目的。
然而此方法,意味着與隱藏類對應的空對象會有龐大的轉換表格。V8透過爲各個建構函數創建隱藏類來處理。若是建構函數不一樣,就算對象的陳述(layout)徹底相同,也會爲它創建一個新的隱藏類。
內嵌緩存
其它的JavaScript引擎和V8不一樣,它們將對象屬性儲存在哈希表中,但V8則將它們儲存在數組中。位移信息-指定個別屬性在數組中的位置-是儲存在隱藏類的哈希表中。同一隱藏類的對象具備相同的屬性名稱。若是知道對象類,那麼就能夠利用位移依數組操做存取屬性。這比搜尋哈希錶快許多。
然而,在JavaScript等動態語言中,很難事先知道對象類型。例如,圖8的原始碼爲對象類型p和q呼叫lengthSquared()函數。對象類型p和q的屬性不一樣,隱藏類也不一樣。所以沒法斷定lengthSquared()函數代碼的參數(arguments)類型。
若要讀取函數中的對象屬性,必須先檢查對象的隱藏類,並有搜尋類的哈希表,以找出該屬性的位移。而後利用位移存取數組。儘管是在數組中存取屬性,要先搜尋哈希表的需求就毀掉了使用數組的優勢。
然而,從不一樣的觀點來看,狀況有所不一樣。在實際的程序中,依賴代碼執行判斷類型的狀況並很少。例如,在圖8的lengthSquared()函數甚至假設大部分經過成爲參數的值,都是Point類對象,而通常而言這是正確的。
function lengthSquared(p) { return p.x* p.x+ p.y* p.y; } function LabeledLocation(name, x, y) { this.name= name; this.x= x; this.y= y; } var p= new Point(10, 20); var q= new LabeledLocation("hello", 10, 20); var plen= lengthSquared(p); var qlen= lengthSquared(q);
圖8 代碼樣本:JavaScript沒法判斷函數參數類型在執行以前根本沒法判斷參數是Point型或是lengthSquared()函數的LabeledLocation型。
內嵌緩存是一項加速技術,此設計是爲了利用程序中局部(local)類別的方法。若要程序化的屬性存取,V8會產生一個指令串來搜尋隱藏類列表(圖9)。此代碼稱爲premonomorphic stub。此stub是爲了在函數存取屬性(圖10)。Premonomorphic stub擁有兩個信息:搜尋用的隱藏類,以及取自隱藏的位移。最後會產生新代碼以緩存此信息(圖11)。
Object* find_x_for_p_premorphic(Object* p) { Class* klass= p->get_class(); int offset = klass->lookup_offset("x"); update_cache(klass, offset); return p->properties[offset]; }
圖9 在僞代碼(pseudocode)中的premonomorphic stub 從隱藏類中取得屬性位移。
圖10 premonomorphic stub呼叫存取函數中的屬性時會呼叫premonomorphic stub。
Object* find_x_for_p_monomorphic(Object* p) { if (CACHED_KLASS == p->get_class()) { return p->properties[CACHED_OFFSET]; } else { return lookup_property_on_monomorphic(p, "x"); } }
圖11僞代碼的monomorphic stub 處理直接嵌入代碼中的位移是用來存取屬性的常數。
在搜尋表格以前,帶有屬性的對象之隱藏類會與緩存隱藏類比較。若是相符就不須要再搜尋,且可使用緩存的位移來存取屬性。若是隱藏類不相符,就透過隱藏類哈希表以通常方式判斷位移。
新產生的代碼被稱爲monomorphic stub。「內嵌」這個字的意思是查詢隱藏類所需的位移,是以當即可用的形式嵌入在所產生的代碼中。當第一次叫出monomorphic stub時,它會將功能從pre-monomorphic stub位址中所叫出的第一個位址重寫成monomorphic stub位址(圖12)。自此,使用高速的monomorphic stub,單靠類比較和數組存取就能夠處理屬性存取。
圖 12 monomorphic stub呼叫 當呼叫monomorphic stub時,它會將功能從premonomorphic stub位址中叫出的第一個位址,重寫成monomorphic stub位址。
若是隻有一個具備屬性的對象,monomorphic stub的效率就會很高。然而,若是類型愈多,緩存失誤就會更頻繁,進而下降monomorphic stub的效率。
當緩存失誤時,V8藉由產生另外一個稱爲megamorphic stub的代碼來解決(圖13)。與個別類對應的monomorphic stub都寫在哈希表中,其在執行時搜尋和叫出stub。若是沒有類型對應的monomorphic stub時,就會從類型哈希表中搜尋位移。
Object* find_x_for_p_megamorphic(Object* p) { Class* klass= p->get_class(); //內嵌處理實際的搜尋 Stub* stub= klass->lookup_cached_stub("x") if (NULL != stub) { return (*stub)(p); } else { return lookup_property_on_megamorphic(p, "x"); } }
圖13僞代碼中的Megamorphic stub處理與類型對應的monomorphic stub事先儲存在哈希表中,並在執行時被搜尋和叫出。若是沒法找到對應的monomorphic stub,就會在類型哈希表中搜尋位移。
當monomorphic stub發生緩存失誤時,monomorphic stub會將功能從monomorphic stub位址叫出的第一個位址以megamorphic stub位址重寫。在代碼搜尋方面,megamorphic stub的性能比monomorphic stub低,可是megamorphic代碼卻比使用緩存更新、代碼生成及其餘輔助處理的premonomorphic stubs快許多。
涵蓋多種類的內嵌緩存稱爲多型態內嵌緩存(polymorphic inline cache)。V8內嵌緩存系統被用來呼叫方法以及存取屬性。
機器語言的特性
如以上所述,V8在設計時使用了例如內嵌緩存等,來達到動態語言中天生的速度。建立使用於內嵌緩存之stub的機器語言生成模塊密切地與JIT編譯器連結。
一些常用的方法也被寫成機器語言以達到與內嵌拓展相同的效果,使它們成爲「內在」的。V8原始碼列出了內在轉換的候選名單。
V8所含的shell程序能夠用來檢查V8所產生的機器語言。所產生的指令串能夠和V8代碼比較,以便顯出它的特性。
例如,在執行圖14a所示的JavaScript函數時,就會產生一個如圖14b所示的x86機器語言指令串。此函數在第39個指令中被呼叫,是個「n+one」加法。在JavaScript中,「+」操做數指示數字變量的加法,以及字符串的連續性。編譯器不是產生代碼來判決這是哪種,而是呼叫函數來負責判斷。
圖14 V8從JavaScript代碼產生的機器語言加法處理被轉換成函數呼叫的機器語言(a、b)。
若是圖14的函數稍作更改(圖15),那圖14b的函數呼叫就會消失,但會有個加法指令(第20),及分支指令(JNZ的若不是零就跳出,第31)。當使用整數做爲「+」操做數的操做數,V8編譯器在不呼叫函數下會產生一個有「加法」指令的指令串。若是發現操做數(在此爲「n」)成了Number對象或String對象等的指標(pointer),
就會叫出函數。「加法」只會發生在當兩個「+」運算的操做數都是整數時。在這種狀況下,由於能夠跳過函數呼叫因此執行就會比較快。
圖15 V8從圖14之JavaScript中所產生的機器語言,經小幅修改
此外,0x2會加上「加法」指令,由於爲最低有效位(least significant bit, LSB)被用來區別整數(0)和指標(1)。加0x2(二進制中的十)就如同在該值加上1,LSB除外。在jo指令的溢位(overflow)處理中,利用測試和jnz指令來斷定指標,跳到下游處理(注1)。
這類的竅門在編譯器中處處都有。然而,產生器代碼也透露了編譯器的限制。具傳統最佳化的編譯器能夠針對圖14和15產生徹底同樣的機器語言,這是因爲常數進位的關係。然而V8編譯器是在抽象語法樹*(abstract syntax tree)單元中產生代碼,所以在處理延伸多個節點時就沒有最佳化。這在大量的push和pop指令也很是明顯。
圖16顯示了C語言裏相同的處理提供參考。因爲C和JavaScript之間的語言規範不一樣,所以所產生的機器語言是圖14和圖15的不一樣,這和編譯器的性能無關。
圖16 C編譯器從C代碼所產生的機器語言所產生的機器語言比V8所產生的乾淨許多(a、b),大部分是由於C和JavaScript語言規範的差別所致。
注1:當溢位信號出現時,jo指令會跳至特定的位址。測試指令將邏輯AND結果反映成零和符號指標等。除非零信號出現,不然jnz指令會跳至特定的位址。
* Abstract syntax tree抽象語法樹:在樹狀架構中表明程序架構的數據。
附錄:熟悉OOP的程序員之參考
也能夠參考:http://blog.csdn.net/horkychen/article/details/7559134
JavaScript沒有類,但爲了讓熟悉使用類(面向對象的代碼)之程序員更方便使用,可使用「new」的操做數來創建對象,就像在Java同樣。在「new」操做數以後會定義一個特別的「constructor」建構函數(圖B-1 a, b)。
然而,即便沒有建構函數,也能夠創建對象(圖B-1c)和設定屬性的(圖B-1 d)。JavaScript對象的屬性和法等隨時均可以新增或刪除。
除了用點標記(dot notation)存取JavaScript屬性之外,也可使用括號,建議散列(hashing)存取(圖B-1 e、f)或是以變量特定屬性名稱字符串(圖B-1 g)。從這些範例中明確顯示JavaScript對象的設計是爲了使用哈希表。
a) 定義建構函數「Point」 function Point(x, y) { // this是指它本身 this.x= x; this.y= y; } b) 當增長新的及呼叫建構器函數時所創建的對象 var p= new Point(10, 20); c) 沒有建構器函數也能夠創建對象 var p= { x: 10, y: 20 }; d) 能夠自由地在對象上新增屬性 p.z= 30; e) 使用點標記存取屬性 var y= p.y f) 使用括號之散列(hashing)存取 var y= p["y"]; g) 也可使用變量進行散列(hashing)存取 var name= "y"; var p[name];
圖B-1 JavaScript代碼範例
本文雖然寫於2009年V8剛剛推出的時候,其中仍對理解V8有很大幫助。
原文地址:http://techon.nikkeibp.co.jp/article/HONSHI/20090106/163615/
繁體中文版地址: http://www.greenpublishers.com/neat/200901/3coverstory.pdf
*本文是以繁體中文版爲基礎從新修訂的。看起來繁體中文版本多爲機翻後人工校訂的,除去兩岸的專業詞彙不一樣外,仍有很多不通的地方。最明顯的就是將class翻譯爲層級。