asm.js 架構與 Emscripten 編譯器:Mozilla 在網頁上發展出接近原生(Native)程式效能的 JavaScript 程式(一)

Mozilla 為了讓 JavaScript 執行得更快,因此發展了 asm.js 這個架構,在這個架構下,可以讓 JavaScript 的程式執行的效率提升很多,甚至可以很接近原生(native)程式的執行效能!

asm.js 本質上是屬於 JavaScript 的一部分,可以看成是簡單版的 JavaScript,在使用上跟一般的 JavaScript 比起來會有些限制,但是執行效能卻非常好,在某些狀況下幾乎可以跟原生程式的執行速度差不多,這樣的執行效能基本上已經可以符合在瀏覽器上任何的應用程式需求了,以下我們將詳細討論相關的細節。

JavaScript 的效能演進

在 2008 年以前,JavaScript 在瀏覽器中的執行效能可以說是非常差勁,但是因為當時會使用 JavaScript 來開發的程式都不大,計算量也非常小,所以在那個時代中,JavaScript 跑的很慢並不會造成困擾。

但在 2008 年左右,因為 web application 的發展,JavaScript 的效能成為一個不小的問題,尤其是在發展大型的 rich application platform 時,更是吃緊。

這時候 Google 釋出了裝載 V8 JavaScript 引擎的 Chrome 瀏覽器,而同一時期,Apple 也發展出具有 Nitro 引擎的 Safari 4,這些引擎的出現帶給 JavaScript 一個展新的時代,讓 JavaScript 透過 JIT(just-in-time)編譯獲得很高的執行效能。這兩種引擎的特點是可以將 JavaScript 轉換為 CPU 可以直接執行的原生程式碼,在這樣的加速之下,可以讓 JavaScript 的執行速度提升三倍以上。

Mozilla 與 Microsoft 也不甘示弱,在 2009 年 Mozilla 在 Firefox 3.5 中加入了 TraceMonkey,而 Microsoft 在 2011 年釋出 Chakra。

雖然 JIT 可以讓 JavaScript 可以有非常高的加速倍率,但是它也有它的限制,而問除是出在 JavaScript 本身的語言特性,造成它很難被最佳化。

在 C 與 C++ 這類的語言中,所有的物件在編譯時都已經可以確定它的類別,而 Java 與 C# 這類的語言增加了一些彈性,但基本上的概念都差不多,這類的程式語言通常稱為強型別(strong type)語言。但是 JavaScript 就不是這樣,它是屬於弱型別(weak type)的語言,其物件型別是可以動態轉換的,這樣的狀況對於 JIT 來說是一大挑戰,若要處理這樣動態的型別轉換,JIT 所產生的可執行程式就必須很保守,這樣才能因應各種轉換的情況,而且也可能造成一些額外的 bugs。

由於 JavaScript 的語言特性,讓瀏覽器的開發者很頭痛,他們極盡所改善 JavaScript 引擎的執行效率,但是最後卻被 JavaScript 語言本身的限制卡住,畢竟 JavaScript 這種語言不是設計拿來做高效能運算的。

突破 JavaScript 效能限制

由於 JavaScript 天生的缺陷,導致大家都往改變 JavaScript 本身的方向來發展,第一個出現的就是 Google Dart,它也是一種 scripting 語言,跟 JavaScript 的用途相同,語法也相似,但是將一些 JavaScript 中會影響效能的部分拿掉。

Google 原本的構想是想將 Dart 整合進瀏覽器,當瀏覽器支援的時候就使用 Dart 引擎執行,碰到不支援的瀏覽器則將 Dart 程式碼轉換為 JavaScript 來執行。Google 自己也發展了一個 Dartium 瀏覽器,它是一個 Chromium 瀏覽器的分支,然後加入 Dart 引擎的成品。

但是就實際面來說,讓網頁與瀏覽器的開發者接受一個全新的語言並沒有那麼容易,而且 JavaScript 行之有年,在短時間之內不可能消失,在這樣的狀況下加入一個新語言只會讓開發過程更加複雜而已。

asm.js

Mozilla 則是以不同的方向嘗試處理 JavaScript 效能問題,他不直接發展新的語言,而是定義一個比較嚴格的 JavaScript 子集合(subset),透過很多的限制,讓你避開很多 JavaScript 中難以最佳化的部分,例如物件導向的建立等,而這個子集合就稱為 asm.js

asm.js 不直接使用 JavaScript 的物件與類別,而是產生一個很長的陣列,使用這個陣列來管理自己記憶體的使用,而這不代表 asm.js 不能使用物件或類別,而是如果要使用物件與類別的話,你必須以類似 C++ 編譯器的方式來使用,在 C++ 程式中,在記憶體中的物件一般是以 v-table(一個儲存類別中所有函數的表格)的記憶體位址與此物件所屬的資料來表示,而在 asm.js 中也是使用類似的方式,在陣列中儲存一連串的物件,而每個物件其實就是 v-table 的陣列索引以及物件的資料。

在 asm.js 所使用的資料形別會比較明確,傳統的 JavaScript 語言中,並不會明確指定數值的型別,一個數值有可能在某些情況是整數,而有些時候是浮點數,其型別會依據不同的操作而有不同,例如 JavaScript 可以允許你對一個浮點數進行位元運算(bitwise operations),當這樣的狀況發生時,其實 JavaScript 會把這個浮點數自動轉換成整數,然後再進行位元運算,由於這樣的轉換是隱式的(implicit),所以這個狀況對於 JIT 而言很麻煩,編譯器無法很安全的將某個數值變數視為某一種固定的型別。而 asm.js 則是使用顯式(explicit)的方式,明確指定某個數值變數應該是整數會是浮點數。

這樣的表示方式是屬於比較低階的寫法,在傳統上的 JavaScript 程式中不常看見,但雖然它的寫法很奇怪,但是它也是 JavaScript 語言,而他那個很長的陣列是使用比較新的 JavaScript 技術:typed arrays,這個技術原本是為了 WebGL 而設計的,但後來每個支援 JavaScript 的瀏覽器都可以使用(包含沒有 WebGL 支援的 Internet Explorer 10 也可以)。至於數值形態的指定,也是使用 JavaScript 本身的語法:「bitwise or with zero」,也就是強迫 JavaScript 將數值視為整數,但不會改變數值本身的值。

由於 asm.js 的設計上就是以 JavaScript 為基礎,它本身就可以在一般的瀏覽器中執行,不像 Dart 語言還需要額外的 Dart 引擎,或是需要將 Dart 轉換為 JavaScript 才能執行,所以 asm.js 在相容性上比較沒有問題。

功能少、效能高

如果瀏覽器本身有明確支援 asm.js 的架構的話,就可以很容易使用它的特性來對使用 asm.js 的程式做最佳化,而支援 asm.js 的 JavaScript 引擎也會知道 asm.js 程式不可以使用哪些 JavaScript 功能,所以它可以產生更有效率的程式。

一般的 JavaScript JIT 必須要考慮很多動態型別的狀況,而 asm.js 因為本來就禁止使用這些功能,所以其 JIT 就可以不用浪費力氣在處理這些問題。

在 asm.js 這樣簡化的架構下,沒有任何動態的轉型、記憶體配置與回收等動作,只剩下少數 well-defined 的整數與浮點數運算子,所以它可以達到非常好的最佳化效能。

因為 asm.js 犧牲了一些功能換取較高的執行速度,所以用它寫出來的程式就有點類似組合語言(assembly language)的感覺,能用的功能很少,所以程式碼常常或寫得又臭又長,而且不好閱讀,很多時候甚至 asm.js 會比真正的組合語言更難用,這可能也是它取名為 asm.js 的原因之一。

事實上 Mozilla 並不是想讓開發者直接使用 asm.js 撰寫程式,而是像一般正常的開發流程一樣,使用其他的高階語言撰寫程式碼之後,再使用編譯器編譯成 asm.js 的程式。

目前最常用的高階語言是 C 或 C++,而用來產生 asm.js 程式的編譯器則是 Emscripten,它也是 Mozilla 的一個專案。Emscripten 是一個以 LLVM 編譯器架構Clang C/C++ front-end 為基礎所發展出來的編譯器,Clang 編譯器會讀取 C 或 C++ 語言的原始碼,並產生一種獨立於平台、類似組合語言的 LLVM IR(LLVM Intermediate Representation),它可以視為可執行程式的半成品,接著再使用後端的程式產生器(backend code generator)來產生真正可以執行的程式,傳統上程式產生器會輸出 x86 的機械碼,但在使用 Emscripten 時,則是會產生 JavaScript 的程式。

Emscripten 有兩種輸出模式可以使用,一種是直接產生一般的 JavaScript,另外一種則是輸出 asm.js 的 JavaScript,但不管是哪一種模式,其輸出的 JavaScript 程式碼都不是給人閱讀用的(會感覺非常雜亂)。而其輸出的一般 JavaScript 程式也是跟 asm.js 的概念類似,都是使用一個很大的陣列來存放所有的資料,而所有的計算與操作都是在這上面進行,透過這樣的方式也可以幫助 asm.js 的開發,而 asm.js 其實就是最後發展出來的標準規格,規定 JavaScript 程式碼應該怎麼寫。

以上就是 asm.js 架構的說明,但究竟 asm.js 的執行速度有多快?接下來有一些使用 Emscripten 的測試報告,可以告訴你 asm.js 與原生程式的差異。

繼續閱讀:asm.js 架構與 Emscripten 編譯器 -- Mozilla 在網頁上發展出接近原生(Native)程式效能的 JavaScript 程式(二)
本站已經搬家了,欲查看最新的文章,請至 G. T. Wang 新網站