瀏覽器可以有多快?

d66g 9年前發布 | 6K 次閱讀 瀏覽器

React.js 以高效的 UI 渲染著稱,其中一個很重要的原因是它維護了一個虛擬 DOM,用戶可以直接在虛擬 DOM 上進行操作,React.js 用 diff 算法得出需要對瀏覽器 DOM 進行的最小操作,這樣就避免了手動大量修改 DOM 的時候造成的性能損失。等等,明明是在中間加了一層,為什么結果反而變快了呢?React.js 的核心思想是認為 DOM 操作是緩慢的,因此可以需要最小化 DOM 操作,以換取整體的性能提升。DOM 操作慢是有目共睹的,而其他 JavaScript 腳本的運行速度就一定快嗎?

瀏覽器可以有多快?

在 V8 出世之前,這個問題的答案是否定的。Google 早年商業模式建立在 Web 的基礎上,當它在瀏覽器中寫出 Gmail 這樣一個無比復雜的 Web app 的時候,它不可能意識不到瀏覽器難以忍受的性能,而這主要是因為 JavaScript 的執行速度太慢。2008 年 9 月,Google 決定自己造一個 JavaScript 引擎來改變這一現狀—— V8。當搭載著 V8 的 Chrome 瀏覽器出現在市場上的時候,它的速度遠遠甩開了當時的所有瀏覽器。瀏覽器性能的空前提升讓復雜的 Web app 成為了可能。

 

近七年過去,瀏覽器的性能隨著 CPU 的性能不斷上升,但再也沒有獲得過 2008 年那樣突破性的增長。V8 到底用了什么樣的技術讓 JavaScript 的性能獲得了如此大的提升呢?

V8 的優化

要說如何讓 JavaScript 變快,就應該先來談談它為什么會慢。眾所周知 JavaScript 是 Brendan Eich 這個家伙用了一周多的時間開發出來的,相比現如今如日中天的 Swift 是 Apple 的一個團隊四年工作的成果,你首先可能就不應該對它有過高的期待。事實上,Brendan Eich 并未意識到自己要開發的是這樣一個體量的語言。為了程序員編寫時的靈活,他將 JavaScript 設計成為弱類型的語言,并且在運行時可以對對象的屬性增添刪改。難倒一大群人的 C++ 中的繼承、多態,還有什么模板、虛函數、動態綁定這些概念在 JavaScript 中完全不存在了。那這些工作誰來做了呢?自然就只有 JavaScript 引擎。由于不知道變量類型,它在運行時做著大量的類型推導工作。在 Parser 完成工作建出一棵抽象語法樹(AST)的時候,引擎會把這棵 AST 翻譯成字節碼(bytecode)交給字節碼解釋器去執行。其中最拖慢性能的一步就是解釋器執行字節碼的階段。回望當時,大家不知道解釋器性能低下嗎?其實不是,這樣設計的原因是當時的人們普遍認為 JavaScript 作為一種給設計師開發的語言(前端工程師有沒有心里一涼?),并不需要太高的性能,這樣做符合成本,也滿足需求。

V8 做的工作主要就是去掉了這個拖慢引擎速度的部分,它從 AST 直接生成了 CPU 可執行的機器碼。這種即時編譯的技術被稱為 JIT (Just in time)。如果你足夠好奇,一個自然的想法就是,這到底是怎么辦到的?

我們舉一個例子來說:

function Foo(x, y) {
    this.x = x;
    this.y = y;
}

var foo = new Foo(7, 8);
var bar = new Foo(8, 7);
foo.z = 9;

屬性讀取

首先是數據結構。你打算如何索引對象的屬性?我們已經太熟悉 JSON 中 key: value 的數據結構,但在內存中可以以 key 來索引嗎?value 在內存中的位置可以確定嗎?當然可以,只要對每個對象維護一個表,里面存著每個 key 對應的value 在內存中的位置就可以了不是嗎?

這里的陷阱在于,你需要對每一個對象都維護這樣一個表。為什么?我們來看看 C 語言是怎么做的。

struct Foo {
    int x, y;
};

struct Foo foo, bar;

foo.x = 7;
foo.y = 8;
bar.x = 8;
bar.y = 7;

// Cant' set foo.z

仔細想想大學時候的教材,foo.x 和 foo.y 的地址是可以直接算出來的呀。這是因為成員 x 和 y 的類型是確定的,JavaScript 里完全可以 foo.x = "Hello" ,而 C 語言就沒辦法這樣做了。

V8 不想給每個對象都維護一個這樣的表。它也想讓 JavaScript 擁有 C/C++ 直接用偏移就讀出屬性的特性。所以它的解決思路就是讓動態類型靜態化。V8 實現了一個叫做隱藏類(Hidden Class)的特性,即給每個對象分配一個隱藏類。對于foo 對象,它生成一個類似于這樣的類:

class Foo {
    int x, y;
}

當新建一個 bar 對象的時候,它的 x 和 y 屬性恰好都是 int 類型,那么它和 foo 對象就共享了這個隱藏類。把類型確定以后,讀取屬性就只是在內存中增加一個偏移的事情了。而當 foo 新建了 z 屬性的時候,V8 發現原來的類不能用了,于是就會給 foo 新建一個隱藏類。修改屬性類型也是類似。

Inline caching

由上可知,當訪問一個對象的屬性的時候,V8 首先要做的就是確定對象當前的隱藏類。但每次這樣做的開銷也很大,那很容易想到的另一個計算機中常用的解決方案,就是緩存。在第一次訪問給定對象屬性的時候,V8 將假設所有同一部分代碼的其他對象也都使用了這個對象的隱藏類,于是會告訴其他對象直接使用這個類的信息。在訪問其他對象的時候,如果校驗正確,那么只需要一條指令就可以得到所需的屬性,如果失敗,V8 就會自動取消剛才的優化。上面這段話用代碼來表述就是:

foo.x
# ebx = the foo object
cmp [ebx,<hidden class offset>],<cached hidden class>
jne <inline cache miss>
mov eax,[ebx, <cached x offset>]

這極大提升了 V8 引擎的速度。

還能更快嗎?

隨著 Intel 宣布 Tick-Tock 模型的延緩,CPU 處理速度不再能像之前一樣穩步增長了,那么瀏覽器還能繼續變快嗎?V8 的優化是瀏覽器性能的終點嗎?

JavaScript 的問題在于錯誤地假設前端工程師都是水平不高的編程人員(如果不是,你應該不會讀到這里),豈圖讓程序員寫得舒服而讓計算機執行得痛苦。在現代瀏覽器引擎已經優化到這個地步的時候,我們不禁想問:為什么一定是 JavaScript ?前端工程師是不是可以讓出一步,讓自己多做一點點事情,而讓引擎得以更高效地優化性能?JavaScript 成為事實上的瀏覽器腳本標準有歷史原因,但這不能是我們停止進步的借口。

當 Web Assembly 正式宣布的時候,我才確定了不僅僅是我一個名不見經傳的小程序員有這樣的想法,那些世界上最頂級的頭腦已經開始行動了。瀏覽器在大量需求的驅動下正在朝著一個高性能的方向前進,瀏覽器究竟可以有多快,2015 可能是這條路上另一個轉折點。

(題圖來自:kendsnyder.com)

 本文由用戶 d66g 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!