正如許多開發者一樣,我也為Asm.js的前景而感到興奮不已。最近的新聞——Asm.js正在被Firefox支援——引起了我的興趣。同樣感興趣的還有Mozilla和Epic聲明(mirror)他們已經為Asm.js而支援Unreal Engine 3——并且運作十分良好。
獲得一個C++遊戲引擎運作Javascript,并使用WebGL來渲染,這是一個重大的突破,這個突破很大程度上歸功于Mozilla開發的工具鍊,才使得這一切變得可能。
由于Unreal Engine 3開始支援Asm.js,我浏覽了來自Twitter,blogs和其他地方的回應,當一部分開發者表現出對這項制造奇迹的技術躍躍欲試時,我也看到許多疑惑:Asm.js是一個插件嗎?Asm.js能讓我平時使用的Javascript運作的更快嗎?它相容所有浏覽器嗎?從這些回應中,我認為Asm.js及其相關技術很重要,我将解釋這些技術,好讓開發者明白發生了什麼以及他們将會如何從這些技術中獲益。另外,為了寫出這篇我對這項技術的概覽,我也請教了David Herman(Mozilla研究院的進階研究員)一大堆關于Asm.js和如何梳理這些知識的問題。
什麼是 Asm.js?
為了了解 Asm.js 及其适用與浏覽器的所在,你需要知道它的由來以及它存在的意義。
Asm.js 來自于 JavaScript 應用的一個新領域: 編譯成JavaScript的C/C++應用. 它是 JavaScript 應用的一個全新流派,由 Mozilla 的 Emscripten項目催生而來。
Emscripten 将 C/C++ 代碼傳入 LLVM, 并将 LLVM生成的位元組碼轉換成 JavaScript (具體的, Asm.js, 是 JavaScript 的一個子集).

如果被編譯成的 Asm.js 的代碼做了一些渲染工作,那麼它幾乎總是由WebGL來處理的 (并且由 OpenGL 來渲染). 這樣技術上就利用了JavaScript 和浏覽器的好處,但幾乎完全避開了頁面中Javascript使用的實際的、正常的代碼執行和渲染路徑.
Asm.js是Javascript的一個子集,它深度的限制了其所能做的範圍和所能操作的對象。這樣做才能使得Asm.js的代碼能夠盡可能運作的快,進而盡可能減少各種假定情況的出現,進而能夠把Asmjs代碼直接轉變成為彙編代碼。有一個特别值得注意的是 - Asmjs還隻是Javascript - 不需要浏覽器的插件或者是别的特性去運作它(雖然一個能夠檢測出并且優化Asmjs代碼的浏覽器當然是要快一些)。它是Javascript的一個特定的子集,為性能優化而生,特别是為那些需要編譯成為Javascript的應用程式來做優化。
最佳的了解Asmjs工作的方式,就是看看一些Asmjs-編譯化的代碼。讓我們看看這個函數,這是從真實的Asmjs編譯子產品裡面提取出來的函數(來自
BananaBread demo)。我對代碼格式做了調整,是以看起來更加合乎代碼片段的閱讀,原本它是一個高度壓縮的JavaScript代碼中的一個很大的計算機二進制對象。
function Vb(d) {
d = d | 0;
var e = 0, f = 0, h = 0, j = 0, k = 0, l = 0, m = 0, n = 0,
o = 0, p = 0, q = 0, r = 0, s = 0;
e = i;
i = i + 12 | 0;
f = e | 0;
h = d + 12 | 0;
j = c[h >> 2] | 0;
if ((j | 0) > 0) {
c[h >> 2] = 0;
k = 0
} else {
k = j
}
j = d + 24 | 0;
if ((c[j >> 2] | 0) > 0) {
c[j >> 2] = 0
l = d + 28 | 0;
c[l >> 2] = 0;
c[l + 4 >> 2] = 0;
l = (c[1384465] | 0) + 3 | 0;
do {
if (l >>> 0 < 26) {
if ((4980736 >>> (l >>> 0) & 1 | 0) == 0) {
break
}
if ((c[1356579] | 0) > 0) {
m = d + 4 | 0;
n = 0;
while (1) {
o = c[(c[1356577] | 0) + (n << 2) >> 2] | 0;
do {
if (a[o + 22 | 0] << 24 >> 24 == 24) {
if (!(Vp(d, o | 0) | 0)) {
break
}
p = (c[m >> 2] | 0) + (((c[h >> 2] | 0) - 1 | 0) * 40 & -1) + 12 | 0;
q = o + 28 | 0;
c[p >> 2] = c[q >> 2] | 0;
c[p + 4 >> 2] = c[q + 4 >> 2] | 0;
c[p + 8 >> 2] = c[q + 8 >> 2] | 0;
c[p + 12 >> 2] = c[q + 12 >> 2] | 0;
c[p + 16 >> 2] = c[q + 16 >> 2] | 0;
c[p + 20 >> 2] = c[q + 20 >> 2] | 0;
c[p + 24 >> 2] = c[q + 24 >> 2] | 0
}
} while (0);
o = n + 1 | 0;
if ((o | 0) < (c[1356579] | 0)) {
n = o
} else {
break
}
}
r = c[h >> 2] | 0
} else {
r = k
} if ((r | 0) == 0) {
i = e;
return
n = c[j >> 2] | 0;
if ((n | 0) >= 1) {
m = f | 0;
o = f + 4 | 0;
q = f + 8 | 0;
p = n;
while (1) {
g[m >> 2] = 0.0;
g[o >> 2] = 0.0;
g[q >> 2] = 0.0;
Vq(d, p, f, 0, -1e3);
n = c[j >> 2] | 0;
if ((n | 0) < 1) {
p = n
} else {
break
i = e;
return
}
} while (0);
if ((c[1356579] | 0) <= 0) {
i = e;
return
f = d + 16 | 0;
r = 0;
while (1) {
k = c[(c[1356577] | 0) + (r << 2) >> 2] | 0;
do {
if (a[k + 22 | 0] << 24 >> 24 == 30) {
h = b[k + 14 >> 1] | 0;
if ((h - 1 & 65535) > 1) {
l = c[j >> 2] | 0;
p = (c[1384465] | 0) + 3 | 0;
if (p >>> 0 < 26) {
s = (2293760 >>> (p >>> 0) & 1 | 0) != 0 ? 0 : -1e3
s = -1e3
} if (!(Vq(d, l, k | 0, h << 16 >> 16, s) | 0)) {
g[(c[f >> 2] | 0) + (l * 112 & -1) + 56 >> 2] = +(b[k + 12 >> 1] << 16 >> 16 | 0);
h = (c[f >> 2] | 0) + (l * 112 & -1) + 60 | 0;
l = k + 28 | 0;
c[h >> 2] = c[l >> 2] | 0;
c[h + 4 >> 2] = c[l + 4 >> 2] | 0;
c[h + 8 >> 2] = c[l + 8 >> 2] | 0;
c[h + 12 >> 2] = c[l + 12 >> 2] | 0;
c[h + 16 >> 2] = c[l + 16 >> 2] | 0;
c[h + 20 >> 2] = c[l + 20 >> 2] | 0;
c[h + 24 >> 2] = c[l + 24 >> 2] | 0
} while (0);
k = r + 1 | 0;
if ((k | 0) < (c[1356579] | 0)) {
r = k
} else {
break
i = e;
return
}
從技術上說,這是Javascript代碼,但是我們已經看到這段代碼一點都不像大多我們正常看到的操作DOM的Javascript。通過看這段代碼,我們可以發現幾件事:
- 這段特定的代碼隻能處理數值。事實上這是所有Asm.js代碼的情形。Asm.js隻能處理被挑出的幾種不同的 數值類型 ,而沒有提供其他的資料類型(包括字元串,布爾型和對象)。
- 所有外部資料在一個稱為堆的對象中存儲并被引用。堆在本質上是一個大數組(應當是一個在性能上高度優化的 類型化數組 )。所有的資料在這個數組中存儲——有效的替代了全局變量,結構體,閉包和其他形式的資料存儲。
- 當通路和指派變量時,結果被統一的強制轉換成一種特定類型。例如f = e | 0;給變量f指派e,但它也確定了結果的類型是一個整數(|0把值轉換成整數,確定了這點)。這也發生在浮點型上——注意0.0和g[...] = +(...)的使用.
- 看看從資料結構中寫入讀出的值,用c表示的資料結構好像是一個 Int32Array (存儲32位整數,因為這些值總是通過|0被轉換成或者轉換自一個整數),g好像是一個 Float32Array (存儲32位浮點數,因為這些值總是通過包裹上+(...)轉換成浮點數)。
這麼做以後,結果就是高度優化,并且可以直接從Asm.js文法轉換成彙編,而不必像常常要對Javascript做的那樣解釋它。它有效地削減了使像Javascript之類的的動态語言緩慢的東西:例如需要垃圾收集器和動态類型。
作為一個更容易了解的Asm.js代碼示例,我們來看看一個
Asm.js規範上的例子:
function DiagModule(stdlib, foreign, heap) {
"use asm";
// Variable Declarations
var sqrt = stdlib.Math.sqrt;
// Function Declarations
function square(x) {
x = +x;
return +(x*x);
function diag(x, y) {
y = +y;
return +sqrt(square(x) + square(y));
return { diag: diag };
看看這個子產品,它完全能讓人了解!閱讀這些代碼,我們能更好的了解Asm.js子產品的結構。一個子產品包含在一個函數中,它以頂部的"use asm";指令開始。它提示解釋器這個函數裡所有的東西可以被當成Asm.js處理,并可以直接被編譯成彙編代碼。
注意在函數頂部的三個參數:stdlib,foreigh和heap。stdlib對象包含了很多
内建數學函數的引用。foreign提供了自定義使用者功能(例如在WebGL中繪制圖形)的通路。最後,heap給了你一個
ArrayBuffer,它可以通過
很多透鏡(例如Int32Array和Float32Array)來觀察。
該子產品剩下的被分成了三部分:變量聲明,函數聲明,還有最後把函數導出暴露給使用者的一個對象。
導出是尤其要去了解的一個重點,因為它既讓所有子產品中的代碼被當成Asm.js處理,又使得代碼可以被其他普通的Javascript代碼使用。是以,在理論上你可以通過使用上面的DiagModule代碼,寫下如下代碼:
document.body.onclick = function() {
function DiagModule(stdlib){"use asm"; ... return { ... };}
var diag = DiagModule({ Math: Math }).diag;
alert(diag(10, 100));
};
這帶來了一個Asm.js子產品DiagModule,它被Javascript解釋器特殊處理,但仍然能夠被其他Javascript代碼使用(我們仍能通路并使用它,比如一個單擊事件處理程式)。
性能如何?
現在Asm.js唯一的實作就是
nightly versions of Firefox(而且也隻是針對特定的幾個平台)。原來的數字告訴我們Ams.js的性能是非常非常不錯的。對于複雜的應用(比如上面的遊戲)性能僅僅比普通C++編譯的慢兩倍(可以和Java或者C#相媲美)。實際上,這已經比目前浏覽器的運作時環境要快很多了,幾乎是最新版的Firefox或者Chrome執行速度的4~10倍。
基于目前最好測試,可以看出Asm.js在性能上有很大的提升。考慮到現在僅僅是Asm.js的最初開發階段,相信在不久的将來就會有更大的性能提升。
看到Asm.js和目前的Firefox和Chrome引擎的性能差距是很有意思的。一個4~10倍的性能差異是非常巨大的(就好像拿這些浏覽器和IE6做性能對比一樣)。有趣的是雖然有這麼大的性能差異,但是許多的Asm.js示範例子仍然是可以在Chrome和Firefox——這些代表着目前Javascript先進技術的引擎——上使用的。這也就是說他們的性能明顯不如一個運作着優化過的Asm.js代碼的浏覽器相提并論。
使用情況
需要說明的是現在幾乎所有基于Asm.js的應用都是C/C++應用使用Emscripten編譯的。可以肯定的說,在不久的将來,這類即将運作在Asm.js的應用,将會從可以在浏覽器中運作這一可移植性中獲益,但是在支援javascript方面有一定複雜度的應用将不可行。
到目前為止,大部分的使用情況下,代碼性能是至關重要的:比如運作遊戲,圖像,處理語言翻譯和庫。從一個關于Emscripten項目清單的概覽可以看到許多即将被廣大開發者使用的技術。
- 許多遊戲引擎已經被支援。一個好的示範也許就是BananaBread FPS遊戲( 源代碼 ),它可以直接在浏覽器中運作,支援多玩家和機器人。
- 支援javascript的LaTeX,即textlive.js,使用Emscripten,允許你完全在你的浏覽器中編輯PDF文檔。
- 支援javascript的SQLite,能夠在Node.js中運作。
- NaCL:一個網絡加密解密的庫。
Asm.js支援
就像之前提到的那樣,現在隻有
nightly版本的Firefox支援Asm.js的優化。
但是,要注意Asm.js格式的Javascript代碼仍然是Javascript,雖然其存在一些限制。這樣,其他的浏覽器即使不支援Asm.js仍可以将其作為普通的Javascript代碼運作。
有關代碼性能重要而令人不解的一點是:如果浏覽器不支援typed array或者不能對Asm.js代碼進行特殊的編譯,Asm.js的性能會變的很差。當然,這并不止針對Asm.js,如果沒有這些特性,浏覽器的性能在其他方面也會收到影響。
Asm.js與Web開發
你可能已經看出來了,上面的Asm.js代碼不是手工輸入的的。Asm.js需要一些特殊的工具來編寫,而且其開發和編寫普通的Javascript代碼有很大差別。目前一般的Asm.js應用都是從C/C++編譯到Javascript的,很顯然它們都不會與DOM進行任何互動,而是直接與WebGL打交道。
為了能讓一般的開發者使用,需要一些更能讓人接受的中間語言。目前
LLJS已經逐漸實作
向Asm.js編譯了。需要注意的是,LLJS這樣的語言同樣與正常的Javascript有很大差別,會讓許多Javascript開發者感到困擾。即使是用LLJS這樣更加友好的語言,編寫Asm.js必須要對複雜的代碼進行優化,這恐怕隻有資深開發者能夠勝任了。
就算有了LLJS或者别的語言來幫助我們編寫Asm.js,我們也沒有同樣性能優異的DOM可以使用。理想的環境應該是将LLJS與DOM一起編譯産生單一的可執行二進制檔案。我還想不出這樣做性能會有多好,但是我想這麼做!
與David Herman的問答
我寫信給David Herman(Mozilla Research的進階研究員),向他詢問了一些問題。他們是如何将Asm.js的各部分結合在一起的?他們又希望使用者從中得到什麼呢?他親切地深入回答了這些問題,有些回複很有趣。我希望你們也同樣能從中獲得啟發。
Asm.js的目标是什麼?你們這個計劃的目标人群是哪些?
我們的目标是讓開放網絡成為一個虛拟主機,使之變為其他語言和平台的編譯目标平台。在最初的版本中,我們集中于編譯較底層的語言:C和C++。我們的長期目标是為更高層的語言提供支援,例如結構對象和垃圾回收等特性。我們最終會讓其支援諸如JVM和.NET之類的應用。
既然asm.js的确擴充了web的基礎,潛在的使用者群很廣。其中一批使用者就是那些想得到盡可能多的運算能力的遊戲開發者。除此之外,有開創性的web開發者總會用一切工具來完成目标,誰能料到他們的辦法呢?是以我真心希望asm.js成為讓我想不到的創新應用得以實作的技術。
建立一個利于使用者通路版本的Asm.js是否合理呢,比如一個更新版本的LLJS?或者是擴充目前項目的範圍,而不僅僅是一個編譯器的目智語言?
絕對可能。事實上,我的同僚James Long最近聲明,他已經從LLJS上開辟了一個初級的分支,用來編譯成為asm.js。在Mozilla 研究所的團隊也試圖和James的工作相配合,進而能夠正式的讓LLJS支援asm.js。
我看法是,一般來說你想手工書寫asm.js代碼的場景非常的少,和任何彙編語言一樣。更多的是,你想用具有豐富表達力的語言,而最終把它們高效編譯為 asm.js。當然,當一種語言達到極緻的表達力,例如javascript,你就會難以預測其性能。(我的朋友 Slava Egorov寫一篇相當好的文章來描述用進階語言寫高性能代碼的挑戰。LLJS的目标是作為一種中間地帶,就像一種相對于asm.js彙編語言的C語言, 這樣就比寫原始的asm.js語言更加容易,而比正常的js有更可預知的性能。但是不象C,它仍然和正常的JS有着良好的互通。這樣,你可以用JS來寫你 的app的大部分,而對于那些高度消耗性能的地方,則可以專注用LLJS來完成。
有一個讨論是關于,在目前支援Asm.js的浏覽器和不支援的浏覽器之間,會出現一種是以更新而形成的性能的分界,類似與2008/2009年中所發生的JavaScript性能競争。雖然技術上來看,Asm.js的代碼在現實中,可以運作于任意兩者之上,而性能的不同,對于大多數的場合而言将會有有明顯的不同(譯者注:原詞是too crippling,意思大緻是嚴重的傷害)。那麼對于這種分界,以及高度限制Javascript,為什麼你選擇了Javascript作為編譯的目标?為什麼不是另一種取代javascript的語言或者是一種插件?(譯者注:作者似乎是想說明,Javascript的限制對于所有浏覽器都會有影響,那麼對于不支援Asm.js的浏覽器會有所傷害,是以為什麼不選擇别的語言或者是插件,這樣就會隻影響自己的産品)
首先,我不認為這種分界如你所定義的那麼嚴重:我們做過一些出色的示範代碼的編譯工作,這些代碼在目前的浏覽器上工作的很好,而且可以得益于asm.js這樣的性能“殺手”。
這 一點是當然的事實,你可以建立一個應用程式,依賴于asm.js所曾益的性能,而且是可用的。與此同時,就如同任何新的web平台的能力,應用程式可以決 定是否降低一些性能,減輕那些與計算密切相關的行為。對于一個應用在降低性能的時候工作,和一個應用完全不能工作是有些不同的。
更廣泛的來看,記得在00年之後開始的浏覽器性能的競争對于今天的web有着顯著的好處,并且應用伴随着浏覽器得到了改善。我相信同樣的事情會而且将發生在asmjs上。
拿Asm.js和Google的Native Client做比較,你覺得如何?他們似乎都有這類似的目标,同時Asmjs有着可以運作在任何支援javascript的地方的優勢。有過對于兩者之間的任何性能比較麼?
嗯,Native Client有點不同,由於它其中配備了平臺相關的匯編代碼;我不認為Google會作為一種web內容技術而支援它(相對於是它用於Chrome Web Store 內容或者 Chrome 擴展而言),或者說,最近沒有。
便攜的Native Client (PNaCI)有著類似的目標,使用平臺無關的LLVM的bitcode來代替原始的匯編代碼。如你所說,asmjs的第一個優勢就是其和現有的瀏覽器兼 容。我們也不必去創建一個系統的接口和重復全部的Web API中所有的接口層,好像Pepper API那樣,由於asmjs訪問目前存在的API是通過直接使用Javascript的。最終,這將對於實現更加輕松有利:Luke Wagner在一個多月中,就把我們第一次實現的OdinMoney的實現版本移植到了Firefox,主要是靠他一個人的工作。因為asmjs沒有一大堆的系統調用和API,而且因為它是在Javascript語法的基礎上建立起來的,你可以重復使用現存的Javascript的引擎和web運行環境提供的全部機制。
我們本可以做一些和PNaCI的性能比較,不過這會有一定的工作量,而且我們更多的是專註於縮小和原始的native代碼性能的距離。我們計劃建立一些自動化的性能測試,這樣就可以用圖表來描述相對於native C/C++編譯器,我們目前的進展。
Emscripten是另一個Mozilla項目,也是Asm.js的主要相容代碼的貢獻者。有多少Asm.js的開發被Emscripten的項目需求所支配?Emscripten又從引擎的提升上獲得了什麼益處?
我們使用Emscripten作為Asm.js的第一個測試用例,通過這種途徑來保證它能夠正确适應實際本地應用的需求。當然Emscripten受益也使他們想要支援的所有擁有本地應用的人受益,比如Epic Games,我們僅在幾天之内就與之組建了開發團隊,通過使用Emscripten和Asm.js來支援web版的Unreal Engine 3的開發。
但是Asm.js能夠使那些專注于底層javascript子集的任何人獲益。舉個例子,我們提到過的開發與Emscripten有相似功能的Mandreel編譯器的folks,我們相信他們可以從Asm.js中受益,就像不久前啟動的Emscripten項目一樣。
Alon Zakai正在編譯我們的基準測試程式,這個程式隻比本地代碼要慢大約2倍,以前我們看到的數字是5倍到10倍或者20倍。這隻是我們最初的OdinMonkey版本,這個版本的asm.js支援Mozilla的SpiderMonkey javascript引擎。在接下來的幾個月中,我希望看到更多的提升。
Asm.js的功能還會變動麼?随着越來越多編譯器開發者的加入,你贊不贊同加入更多的附加功能(比如更進階的資料結構)?
當然。Luke Wagner在Mozilla wiki上寫了 asm.js與 OdinMonkey路線圖 ,裡面讨論了我們未來的計劃——我必須指出這裡面沒有什麼已經确定了的但的确說出了我們正在努力的方向。我很願意加入對ES6 結構對象 的支援。這将會提供垃圾回收機制與良好的類結構,幫助像 JSIL 這類編譯器将C#和Java編譯為Javascript。我們也希望加入ES7 ,這将提供32位浮點數和64位整數支援,也許還會為提供 SIMD 支援加入定長向量。
可以做出JavaScript-to-Asm.js轉譯器麼,它會被做出來麼?
可以做出來,但會做麼?不一定。想想 盜夢空間中你每次進行夢中夢的情景,時間将會變得多慢?一樣的道理,你要是想在JS中運作一個JS引擎那一定會非常慢的。我們不嚴格地算一算,如果asm.js比原生代碼慢一倍,那在asm.js中運作一個JS引擎,這引擎将會比它正常的速度慢一倍。
當然,你總是可以在一個JS引擎中運作另一JS引擎,誰知道到底會怎樣呢?現實中的性能從來就不像理論計算那樣明确。我歡迎一些積極的hacker去嘗試它。事實上,斯坦福的學生
Alex Tatiyants 已經用Emscripten 将Mozilla的SpiderMonkey引擎編譯為JS 了——你所需做的隻是設定 Emscripten編譯器參數 讓它産生asm.js代碼,時間比我充裕的家夥可以嘗試一下……
現在Asm.js還不能進行與有關DOM和浏覽器的操作。建立一個Emscripten到Ams.js版本的DOM(就像DOM.js)如何?
這是一個漂亮的想法。這對目前剛剛起步的asm.js有點難度,現在我們還不支援任何DOM對象。在我們不斷為asm.js添加ES6類型的對象時,這種想法有可能變得可行而并且更加有效。
這方面一個比較酷的應用可能就是看看到底web平台能多有效的執行自承載業務。建立DOM.js一個動機就是想要看一下純JS實作的DOM能否擊敗傳統、低效的出入隊列式的、跨堆棧的JS堆與相關C++ DOM對象之間的記憶體管理方法。有了asm.js的支援,DOM.js也許可以在性能上勝過那些高度優化過的資料結構。這值得一試。
編寫Asm.js代碼與編寫一般的Javascript代碼相比确實十分困難,你有什麼工具可以提供給開發者和編譯器作者呢?
首先最重要的是,我們有LLJS這樣的語言。就像你提到的那樣,可以将其編譯為Asm.js。同樣,我們在将其使用于web時也會有一些挑戰,例如使用source maps之類的技術将生成的代碼對應到浏覽器開發者工具中的原始代碼。我很高興看到source maps的發展,它已經可以回報更加豐富的調試資訊。我們要在在source maps的極小尋址資訊和格式複雜的調試資訊(DWARF)之間進行取舍,這是一個代價平衡的問題。
對asm.js而言,我認為近期我們将會将精力集中在LLJS上,同時我也一直歡迎開發者告訴我們一些能夠改善他們程式設計體驗的建議。
我覺得你們很樂于和其他浏覽器廠商一起工作,你們的合作和讨論進展如何?
沒錯。我們之間有過一些非正式的讨論,他們一直對我們給予鼓勵,我相信我們能做的更好。我很樂觀,我們可以與很多廠商一起發展asm.js,那時它可以讓我們輕松地在不改變架構的情況下開發應用。就像我說的,事實上Luke隻用了幾個月就開發出了OdinMonkey,這很令人激動。我很高興收到V8引擎下asm.js的bug報告。
更重要的是,我希望開發者可以檢查asm.js的源碼,看看我們是怎麼想的,并回報給我們和其他浏覽器廠商。