競速(三):JavaScript編譯器策略

伯樂線上讀者發表於2013-06-24

伯樂線上注:英文原文:John Dalziel,感謝@AvisBlume 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。以下是譯文。

JavaScript語言廣受歡迎的原因很多。首先它分佈廣泛,其次從開發人員的角度來看,它很快很靈活。JavaScript中的一切都是類,因此快速的建立結構是件非常容易的事,並且完全不需要定義資料型別,因為所有的型別都是可推斷的。但也正是這種通用性給編譯工作帶來了挑戰。

 

隱藏類

雖然在JavaScript中建立類和層次結構十分簡單,但是對於編譯器來說要遍歷這些複雜的結構速度會很慢。在C語言中,要儲存和讀取屬性和屬性值通常會使用雜湊表或者字典。這是一種陣列樣的結構,通過檢索唯一的表示屬性名字的字串,可以將對應的屬性值找出來。而問題在於大型雜湊表中這一過程可能會很慢。

為了提高檢索速度,V8和SpiderMonkey都實現了隱藏類-類在後臺的影子。Google將其稱為map,Mozilla將其稱為shape,但其實是差不多的東西。這種結構的搜尋速度要比標準的字典結構快很多。

 

型別推論

JavaScript中的動態型別允許同一個屬性在一個地方是Number型別的,而在另一個地方是String型別的。很不幸的是這種通用性會要求編譯器建立更多的型別檢查條件,而這些條件程式碼要比型別確定的程式碼大很多也慢很多。

解決這一問題的方法稱為型別推論,現在所有的JavaScript編譯器都使用這一方法。編譯器檢查程式碼並且對某個屬性的資料型別做出一個假設,如果這個推論是正確的,那麼就執行有型別的即時編譯,將生成一段快速的資料型別確定的機器程式碼存根;如果型別推測不正確,那麼這段程式碼將會“失效”而進行無型別的即時編譯,利用速度較慢的條件程式碼來使其變得完整。

 

內聯快取

現在的JavaScript編譯器中最常見的優化手段是內聯快取技術。該技術30年前在Smalltalk編譯器中首次被實現,雖然年代久遠,但還十分有用。

內聯快取技術需要用到我們已經討論過的型別推論和隱藏類。編譯器遇到一個新的物件時就會在快取中生成它的隱藏類,類中的某些成員的型別可能是推論型別。之後如果在程式碼別處遇到,那麼就可以將其和快取中的版本進行快速的比較。如果兩者匹配,那麼之前生成的優化過的機器程式碼存根就能拿來重用。如果結構或者成員的資料型別已經發生改變,那麼可以使用較慢的通用程式碼。或者像現在的有些編譯器那樣,甚至能夠執行多型內聯快取,也就是會為每種資料型別都生成一個結構相同的機器程式碼存根。

如果你想對JavaScript中的內聯快取技術有進一步的瞭解,我推薦閱讀下Google V8編譯器的工程師Vyacheslav Egorov的文章。他用JavaScript寫了一個Lua語法分析器,並非常詳細地解釋了內聯快取技術。

 

一旦編譯器弄清程式碼結構以及其中的資料型別,它將能夠進行全面的優化。下面是幾個優化手段:

內聯展開

函式呼叫的計算成本很高,因為它們需要執行某種查詢,而查詢可能會很慢。內聯展開的思想是將被呼叫函式的函式體完整地插入呼叫該函式的地方。這能避免分支,生成更快速的程式碼,但是要付出額外的儲存空間代價。

迴圈不變數程式碼移動

迴圈是主要的優化物件。將不必要的計算移出迴圈可以大幅提升表現。最常見的例子是在for迴圈中,每次迴圈都計算陣列長度的話將會十分多餘。長度可以預先計算出來存在一個變數裡,計算長度的操作可以移出迴圈。

常量摺疊

有些常量或變數的值在程式的生命週期中都不會發生改變,常量摺疊會將這些量的表示式的值預先計算出來並直接使用這些值。

公共子表示式消除

和常量摺疊類似,這項優化會在掃描程式碼之後將計算結果相同的表示式找出來,然後用一個存有該計算結果的變數代替這些表示式。

死程式碼消除

死程式碼是指不會用到的或者不會執行到的程式碼。如果在程式中有個函式從來沒有被呼叫過,那麼絕對沒必要去對它進行另外的優化,可以直接安全地將其消除掉,這也能減少程式的大小。

 

這些優化手段僅僅是千里之行第一步。要讓JavaScript跑得和native C一樣快。這個目標真的能實現嗎?在最後一節中我們將會看到一些已經跑得很快的JavaScript專案。

 

英文原文:John Dalziel,編譯:@AvisBlume

譯文連結:http://blog.jobbole.com/41918/

【非特殊說明,轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊,謝謝合作!】

相關文章