上一篇的測試報告中,我們分析過基本的 asm.js 程式執行效能,大致上還不錯,這裡我們繼續討論更進階的主題。

進階測試結果

但很不幸的,實際情況往往會比較複雜,在你要使用 asm.js 架構開始開發程式或是跟同事大力推銷 asm.js 之前,你必須先注意幾個問題。

最簡單的問題就是:asm.js 的高效能目前只有在 Firefox 中才能達到,目前最新版的 Firefox 22 才剛剛加入 asm.js 的功能,至於其他的瀏覽器當然就無法支援 asm.js 這個架構了,而最慘的是:根據我們的測試結果,asm.js 在不支援 asm.js 架構的瀏覽器中,效能反而更差!

之前我們看過原生程式與 Firefox 中一般 JavaScript 與 asm.js 的效能比較,接著我們來看 Internet Explorer 的效能如何。

classic-native-v-firefox-v-ie

stream-native-v-firefox-v-ie

這裡可以看出來,Chakra(IE 所使用的 JavaScript 引擎)的執行速度比 Firefox 慢(至少由 Emscripten 產生的程式是這樣),只有 n-body 這個測試例外,但這不代表 Chakra 在實際的應用上會比較慢,畢竟由 Emscripten 所產生的 JavaScript 程式碼與一般的 JavaScript 還是有一些差異的,所以在 Chakra 上跑得比較慢其實也不意外。

benchmark-game-native-v-firefox-v-ie

這裡也可以看出在大部分的情況下,asm.js 讓情況變得更糟糕,從原本的 6.4 倍原生程式執行時間增加到 6.8 倍,多出 6% 的執行時間。

因此,如果開發者想要開發出一個執行效能還不錯的網頁應用程式,asm.js 這個架構在目前來說可能不是最好的選擇。

Emscripten 的程式也有記憶體不足的問題,我們測試程式裡面最大的應該就是 STREAM,它 allocate 了大約 240MB 的記憶體空間來存放他的測試資料,重點是他的原生程式所使用的記憶體不到 256MB,而因為 asm.js 的記憶體配置必須是 2 的整數次方,照道理說應該可以在使用 256MB 記憶體大小的狀況下來執行,但是實際上 256MB 不夠,它需要下一個大小的記憶體空間,也就是 512MB。這樣一來,記憶體的用量比較大再加上記憶體配置的大小限制在 2 的整數次方,可能會造成一些程式需要配置到很大的記憶體,多出來的部分就浪費掉了。

這些問題可望在未來可以獲得解決,在今年的 Google I/O 研討會上至少有一家瀏覽器表示對於 asm.js 有興趣,Google 也表示它們所做的最佳化可以讓 asm.js 的效能上升 2.4 倍,有了 Google 的幫助,Chrome 瀏覽器的問題應該就可以決了。

平台限制

Emscripten 目前並沒有提供完整的執行環境,由於他是在瀏覽器中執行的,所以它還是缺乏某些功能(例如完整的網路功能)。另外它的函式庫也有一些問題,例如用來取得系統時間的 POSIX/UNIX 的標準函數 clock_gettime() 雖然有在 Emscripten 中,但是卻不能使用(它永遠只會傳回 0),雖然另外一個 gettimeofday() 函數可以正常使用,以這個狀況來說問題不大,但這也表示 Emscripten 還有許多地方需要再修改。

除了上面講的時間函數之外,Emscripten 也還有一些比較嚴重的問題,例如執行序的部份,Emscripten 在某方面來說就顯的綁手綁腳,傳統的 JavaScript 同時間只能有一個執行序在執行,在比較新的 Web Workers 標準中,雖然加入一些多執行序的功能,但是功能也還是非常有限,Web Workers 中的執行序不能互相交換資料,所以能做的事情很有限,但除用用這樣的方式以外,也很難找到一個方法讓平行化更容易且維持較好的執行效率。

因此 Emscripten 只能用在單一執行序的程式上,這在我們的測試中顯然對於 Emscripten 不太公平,所以原生程式在測試時,我們就不使用多執行序的版本(當然也是可以使用,不過這樣只是讓測試結果更懸殊而已)。

benchmark-game-native-v-openmp-v-asmjs

多執行序的 binary-trees 與 mandelbrot 與單一執行序的版本基本上程式是一樣的,只是在迴圈上加上一行 OpenMP 的指令而已,這種平行化的方式很簡單,但是因為這個問題的特性,縱使使用這樣簡單的方式也可以得到很好的加速倍率,跟多執行序的原生程式比較之下,asm.js 程式的執行時間是 binary-trees 的 2.87 倍、mandelbrot 的 12.2 倍。

Emscripten 也沒有類似SSE 與 AVX 的 SIMD(Single Instruction, Multiple Data)功能,因為 Firefox 的 JavaScript 引擎根本不會產生向量化的程式碼,而我們在編譯原生程式時所使用的編譯器確實會使用 SSE2,但是它在大部分的狀況下只會用到 scalar(Single Instruction, Single Data)的功能而已,只有 LINPACK 與 STREAM 這兩個程式會使用到向量化的指令集。

使用向量化的指令集理所當然會有較高的效能,Benchmark Game 包含了一個 SSE3 版本的  fannkuch-redux 測試程式,這種平行化比 OpenMP 更複雜,它幾乎需要改寫整個程式,但是效能的提升會更明顯:

native-code-game

使用 SSE3 的最佳化版本所使用的執行時間只有一般版本的 40%,而再與 asm.js 相較之下,落差就更大了,asm.js 比起 SSE3 的版本,需要 3.7 倍的執行時間。

我們手上並沒有多執行序或 SIMD 版本的原始程式碼,我們只拿到封閉原始碼的二進位執行檔,所以無法保證測試出來結果的可靠度與正確性,但至少可以看出最佳化版本與一般單執行序版本之間的差異:

classic-native-v-optimized-v-asmjs

stream-native-v-optimized-v-asmjs

如果你對於這方面有興趣,Intel 官方撰寫的 LINPACK 最佳化版本可以從它的網站上下載,而很普遍被使用的標竿分析工具 SiSoftware Sandra 中也有包含多執行序且經過最佳化的 Whetstone(使用 SSE4)與 Dhrystone(使用 SSE3),其中也包含很多記憶體的標竿分析,其中一個就是最佳化過後的 STREAM。

基本上在一些沒有著重於效能與最佳化的一般程式上,asm.js 才可以展現它的長處,但如果一個程式原本的效能就已經非常好了,像使用多執行序或是 SIMD 平行化之後的程式,那麼使用 Emscripten 與 asm.js 之後,縱使他還是可以執行,但是效能跟原生程式比較起來就會很糟糕。

很明顯的,如果 asm.js 想要跟這些平行化的程式並駕齊驅,那麼就要開發許多針對高效能運算的功能,像是 shared memory multithreading 等,但是這樣的架構將會大幅改變現有的瀏覽器架構,也會牽涉到網頁中其它的功能(例如 WebGL 等),是個浩大的工程,沒有那麼容易做到,以目前來看大概不太可能在短時間內實現。

麻煩的開發流程

我們很幸運的在一開始就拿到可以正常運作的程式碼,所以可以省去除錯等相關的工作,否則依照 Emscripten 目前的功能,開發起來可能會很辛苦,因為不論是否使用 asm.js,他還是欠缺許多該有的除錯功能。

Emscripten 所產生的 JavaScript 程式碼非常大,例如 binary-tree 的原生程式有 16,896 bytes,而一般的 JavaScript 版本則有 379,784 bytes,asm.js 的話則是 667,207 bytes,一般的瀏覽器並不是為了這種程式碼而設計的,所以當你使用瀏覽器的除錯工具時,這麼大的 JavaScript 檔案開起來會非常慢(無論是 Firefox、Chrome 或是 Internet Explorer 都一樣),而且用起來也不穩定。

基本上這個問題其實也不是很重要,因為大部分的情況你不會由 JavaScript 的程式碼來除錯,因為 Emscripten 所產生的程式碼雖然比組合語言好懂一些,也確實有函數與函數呼叫的結構,但是除此之外,你大概也沒辦法看懂什麼,要從 JavaScript 來除錯其實不是一個好辦法。

針對這個除錯的問題,目前已經有一項技術正在發展,就是 Source Maps,這個工具可以讓你在除錯時同時看到編譯過的程式與原始碼之間的對應,但是基本的除錯技巧與經驗還是必要的。原生碼的除錯器是一個比較複雜的工具,它可以在組合語言與原始碼之間切換、逐步執行與觀看記憶體中的資料等,而 JavaScript 的除錯器因為還不成熟,所以使用上可能不是那麼順手,再加上 asm.js 的因素,可能會更雪上加霜。

以目前的情況來說,要除錯最好的方式是使用一般原生碼的除錯工具,把錯誤都修正完成之後,再使用 Emscripten 編譯成 JavaScript 程式碼,這樣可以避開在 JavaScript 中除錯的程序,但是如果你的程式有一部分是使用 Emscripten 編譯的,而另一部分是手寫的 JavaScript 程式的話,就比較難這樣使用了。

編譯過程所花費的時間也是 Emscripten 的問題之一,在編譯成 node.js 與非 asm.js 程式的過程中,會進行很多的最佳化步驟,這使得編譯的過程變得很花時間,跟原生程式的編譯時間比起來多了 10 倍到 50 倍,這也是開發過程中一個惱人的問題。

asm.js 還是不錯的!

asm.js 基本上已經夠快了,我們測試用的 PC 並不是世界上最快的電腦,但是它已經算是非常快的了,根據 Steam Hardware Survey 的資料,即使跟一些遊戲用的電腦相比,我們的配備還是在前 15% 左右,即使程式跑的稍微慢一些(多出 26% 的執行時間),整題而言還是不錯的。

但如果我們使用比較老舊的電腦,會得到一樣的結論嗎?個人感覺是不會,因為在很慢的電腦中多出 26% 的執行時間可能讓感覺還可以的程式變成很慢。這個問題在一些手持裝置上更是明顯,多出 26% 的執行時間意味著 CPU 要多跑這麼久,也就是要多浪費這麼多的電力,這對於電力有限的裝置而言是一大致命傷。

基本上我們大概不會想要把手直裝置上的原生程式替換成 asm.js,但是如果將傳統的網頁應用程式用 asm.js 取代可能是比較可行的,一般來說 asm.js 的效能會比 Emscripten 所產生的一般 JavaScript 來的好,跟手寫的 JavaScript 比起來又更好一些。

我們沒有每一個 Benchmark Game 測試程式的手寫 JavaScript 版本,這裡只能嘗試測試幾個例子,這些測試是在 jsshell 中執行的:

benchmark-game-human-v-asmjs

這些結果的變異非常大,其中兩個手寫的 JavaScript 比 asm.js 慢,而另外兩個反而比較快。

整體而言,asm.js 跟原生程式比較起來還是遜色不少,但是作為一般 JavaScript 的輔助工具算是很不錯的選擇。

參考資料:ars technica