現在的JavaScript代碼要進行性能優化,通常使用一些常規手段,如:延遲執行、預處理、setTimeout等異步方式避免處理主線程,高大上一點的會使用WebWorker。即使對於WebWorker也僅僅是解決了阻塞主線程的問題,但是對於JavaScript計算性能慢的問題並沒有解決。這里對一些需要密集計算的場景我給大家推薦一個神器——WebAssembly。在目前階段,WebAssembly 適合大量密集計算、並且無需頻繁與 JavaScript 及 DOM 進行數據通訊的場景。比如游戲渲染引擎、物理引擎、圖像音頻視頻處理編輯、加密算法等
WebAssembly是什么?
WebAssembly是一種運行在現代網絡瀏覽器中的新型代碼並且提供新的性能特性和效果。它設計的目的不是為了手寫代碼而是為諸如C、C++和Rust等低級源語言提供一個高效的編譯目標。WebAssembly的模塊可以被導入的到一個網絡app(或Node.js)中,並且暴露出供JavaScript使用的WebAssembly函數。JavaScript框架不但可以使用WebAssembly獲得巨大性能優勢和新特性,而且還能使得各種功能保持對網絡開發者的易用性。這是來自MDN的介紹。但你是不是看了官方介紹也不知道WebAssembly到底是個什么東西呢,沒關系開始我也這么覺得。簡單來說WebAssembly就是瀏覽器提供的一項直接運行二進制機器代碼的能力。這些機器代碼怎么來呢,是通過C、C++或Rust等語言編譯來的。
那么如何編譯呢,首先你得學會寫C語言代碼,然后你得用一系列工具把它編譯成二進制代碼。這個過程絕對不會是一帆風順的,因為根據我摸爬滾打的經驗來說,這玩意兒從頭到尾都是坑。WebAssembly的編譯過程需要用到以下工具:
哦對了,還要裝Visual Studio2015,千萬別看這vs17新就裝了個17,因為Emscripten目前跟vs15的結合性最好。
安裝工具鏈
在所有工具鏈之前,需要安裝下面幾個工具:
- Git
- CMake
- Visual Studio Community 2015 with Update 3 or newer
- Python 2.7.x
然后下載編譯Emscripten,這玩意兒需要翻牆,然后特別大,得慢慢等個半小時差不多,然后千萬別按照它官網的介紹來安裝,要按照MDN上的方式來安裝,這樣安裝下來直接帶有Binaryen了(我只會Windows平台的配置):
git clone https://github.com/juj/emsdk.git cd emsdk # on Linux or Mac OS X ./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit ./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit # on Windows emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
先克隆,克隆之后打開文件夾,運行里面的emcmdprompt.bat,打開的命令行里面可以運行install和active命令:
emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
然后添加幾個環境變量:
D:\emsdk-portable\binaryen\master_vs2015_64bit_binaryen\bin;d:\emsdk-portable;d:\emsdk-portable\clang\fastcomp\build_incoming_vs2015_64\Release\bin;d:\emsdk-portable\node\4.1.1_64bit\bin;d:\emsdk-portable\python\2.7.5.3_64bit;d:\emsdk-portable\java\7.45_64bit\bin;d:\emsdk-portable\emscripten\incoming;d:\emsdk-portable\binaryen\master;
在實際的運行中你可能遇到這個錯誤:
CMake does not find Visual C++ compiler
那么你需要新建一個Vs15的c++工程,按照這里說的運行一下:Stack Overflow
I have found the solution. While Visual Studio IDE installed successfully it did not install any build tools and therefore did not install the C++ compiler. By attempting to manually create a C++ project in the Visual Studio 2015 GUI I was able to prompt it to download the C++ packages. Cmake was then able to find the compiler without any difficulty.
這樣一些跟WebAssembly相關的常見命令就可以運行了,本文的不詳細解釋WebAssembly,只說一些踩過的坑,具體比較詳細的我認為這篇文章很不錯——WebAssembly 實踐:如何寫代碼。里面的代碼在這里——wasm-examples.我們來看一些性能比較,大家同時運行斐波那契數列:
// JavaScript版本 function () { function fib (n) { if (n < 2) { return 1 } return fib(n - 2) + fib(n - 1) } return { fib } } // C版本 int fibonacci (int n, int a, int b) { if (n <= 2) { return b; } return fibonacci(n - 1, b, a + b); } int fib (int n) { return fibonacci(n, 1, 1); }
那么它們的性能對比如下:
一般來講斐波那契數列計算到40已經是很大的運算量了,可以看出由C直接轉化成wasm二進制機器碼的計算性能比純原生js快了接近70%。有的同學可能會覺得這里應該使用尾遞歸優化,我做過實驗,尾遞歸優化的js在計算40時,差不多是幾毫秒,但是同樣尾遞歸優化的c編譯成wasm幾乎是0。
WebAssembly實際應用
通常來講在普通的前端業務中根本不需要使用WebAssembly,但是在一些需要極大的計算能力的場景,比如Web地圖、WebAR、Web圖像識別中傳統方案下js的計算性能無法達到要求,那么這時候就是WebAssembly展現應用能力的時候了。對我們來說在實際中最有用的方式不是簡單計算一個數值,而是希望用c來處理一批數據,然后在JavaScript側能夠通過ArrayBuffer形式使用。對此百度地圖團隊有一篇文章——地圖引擎使用WebAssembly的經驗分享(WebGL地圖引擎技術番外篇之二)專門介紹了它們的一個使用場景。
這里呢它們提供了一種實踐路線:
方案三:C/C++編譯 目前主流方案是使用 Emscripten 將 c/c++ 代碼編譯成 asm.js 或 wasm。一開始沒有使用 emscripten 主要是調研期間遇到了兩個問題: ONLY_MY_CODE 模式編譯出的 asm.js 代碼不是合法的 asm.js 代碼 emscripten 默認的編譯模式會將一些依賴庫代碼以及加載 asm.js 的代碼一起編譯出來,這對於我們這種只需要一個 asm.js 模塊的需求來說是不必要的。emscripten 有ONLY_MY_CODE模式可以選擇,顧名思義,這個模式就是只會編譯出模塊代碼而不會增加其他多余代碼。但是在調研過程中發現這個模式編譯出的 asm.js 代碼的類型標注有問題,導致代碼不合法。 解決方案:官方 github 給出的解答是 ONLY_MY_CODE 模式沒有經過嚴格的測試,可以使用 SIDE_MODULE 選項來達到類似的效果。經過測試這個模式雖然也會增加少量額外代碼但是可以解決問題。 emscripten 直接編譯 wasm 要求分配內存大於 16MB emacripten 加上-s WASM=1可以支持直接編譯 wasm,但是強制要求分配的內存大於16MB,這對於一些不需要這么大內存的模塊來說會造成浪費。 解決方案:放棄使用 emscripten 編譯 wasm,現在采用的編譯步驟是: 使用 emscripten 將 c++ 代碼編譯成 asm.js 使用 binaryen 將 asm.js 編譯成與 wasm 等價的文本格式 wat 代碼 使用 wabt 將 wat 代碼編譯成二進制 wasm 解決了這兩個問題之后,采用此方案就可以達到寫一次代碼編譯同時得到 asm.js 和 wasm 的目的了。
然而很不幸,這條路在我實踐過程中走不通,無論怎樣Emscripten都無法產出純凈的asm代碼,總是會帶有一些膠水代碼。比如C代碼:
// 分配內存,此數組占0x1000*4=16384byte static int s_array[0x1000]; static int s_current_index = 0; int* get_start(); int* get_end(); void generate_array(int); // 暴露給JS使用,得到數組的開始偏移量 int* get_start() { return s_array; } // 暴露給JS使用,得到數組的結束偏移量 int* get_end() { return &s_array[s_current_index]; } // 將生成的數組放進內存中 void generate_array(int count) { for (int i = 0; i < count; ++i) { s_array[i] = i; } s_current_index = count; }
最終經過Emscripten編譯成的Asm.js是如下代碼:

// Capture the output of this into a variable, if you want (function(fb, parentModule) { var Module = {}; var args = []; Module.arguments = []; Module.print = parentModule.print; Module.printErr = parentModule.printErr; Module.cleanups = []; var gb = 0; // Each module has its own stack var STACKTOP = getMemory(TOTAL_STACK); assert(STACKTOP % 8 == 0); var STACK_MAX = STACKTOP + TOTAL_STACK; Module.cleanups.push(function() { parentModule['_free'](STACKTOP); // XXX ensure exported parentModule['_free'](gb); }); // === Auto-generated preamble library stuff === //======================================== // Runtime essentials //======================================== // === Body === var ASM_CONSTS = []; gb = Runtime.alignMemory(getMemory(16400, 4 || 1)); // STATICTOP = STATIC_BASE + 16400; /* global initializers */ __ATINIT__.push(); /* memory initializer */ allocate([], "i8", ALLOC_NONE, gb); /* no memory initializer */ // {{PRE_LIBRARY}} var ASSERTIONS = true; // All functions here should be maybeExported from jsifier.js /** @type {function(string, boolean=, number=)} */ function intArrayFromString(stringy, dontAddNull, length) { var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; var u8array = new Array(len); var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); if (dontAddNull) u8array.length = numBytesWritten; return u8array; } function intArrayToString(array) { var ret = []; for (var i = 0; i < array.length; i++) { var chr = array[i]; if (chr > 0xFF) { if (ASSERTIONS) { assert(false, 'Character code ' + chr + ' (' + String.fromCharCode(chr) + ') at offset ' + i + ' not in 0x00-0xFF.'); } chr &= 0xFF; } ret.push(String.fromCharCode(chr)); } return ret.join(''); } Module["intArrayFromString"] = intArrayFromString; Module["intArrayToString"] = intArrayToString; var setTempRet0 = Runtime.setTempRet0, getTempRet0 = Runtime.getTempRet0; Module.asmGlobalArg = { "Math": Math, "Int8Array": Int8Array, "Int16Array": Int16Array, "Int32Array": Int32Array, "Uint8Array": Uint8Array, "Uint16Array": Uint16Array, "Uint32Array": Uint32Array, "Float32Array": Float32Array, "Float64Array": Float64Array, "NaN": NaN, "Infinity": Infinity }; Module.asmLibraryArg = { "abort": abort, "assert": assert, "enlargeMemory": enlargeMemory, "getTotalMemory": getTotalMemory, "abortOnCannotGrowMemory": abortOnCannotGrowMemory, "abortStackOverflow": abortStackOverflow, "setTempRet0": setTempRet0, "getTempRet0": getTempRet0, "DYNAMICTOP_PTR": DYNAMICTOP_PTR, "tempDoublePtr": tempDoublePtr, "ABORT": ABORT, "STACKTOP": STACKTOP, "STACK_MAX": STACK_MAX, "gb": gb, "fb": fb }; // EMSCRIPTEN_START_ASM var asm = (/** @suppress {uselessCode} */ function(global, env, buffer) { 'almost asm'; var HEAP8 = new global.Int8Array(buffer); var HEAP16 = new global.Int16Array(buffer); var HEAP32 = new global.Int32Array(buffer); var HEAPU8 = new global.Uint8Array(buffer); var HEAPU16 = new global.Uint16Array(buffer); var HEAPU32 = new global.Uint32Array(buffer); var HEAPF32 = new global.Float32Array(buffer); var HEAPF64 = new global.Float64Array(buffer); var DYNAMICTOP_PTR=env.DYNAMICTOP_PTR|0; var tempDoublePtr=env.tempDoublePtr|0; var ABORT=env.ABORT|0; var STACKTOP=env.STACKTOP|0; var STACK_MAX=env.STACK_MAX|0; var gb=env.gb|0; var fb=env.fb|0; var __THREW__ = 0; var threwValue = 0; var setjmpId = 0; var undef = 0; var nan = global.NaN, inf = global.Infinity; var tempInt = 0, tempBigInt = 0, tempBigIntS = 0, tempValue = 0, tempDouble = 0.0; var tempRet0 = 0; var Math_floor=global.Math.floor; var Math_abs=global.Math.abs; var Math_sqrt=global.Math.sqrt; var Math_pow=global.Math.pow; var Math_cos=global.Math.cos; var Math_sin=global.Math.sin; var Math_tan=global.Math.tan; var Math_acos=global.Math.acos; var Math_asin=global.Math.asin; var Math_atan=global.Math.atan; var Math_atan2=global.Math.atan2; var Math_exp=global.Math.exp; var Math_log=global.Math.log; var Math_ceil=global.Math.ceil; var Math_imul=global.Math.imul; var Math_min=global.Math.min; var Math_max=global.Math.max; var Math_clz32=global.Math.clz32; var abort=env.abort; var assert=env.assert; var enlargeMemory=env.enlargeMemory; var getTotalMemory=env.getTotalMemory; var abortOnCannotGrowMemory=env.abortOnCannotGrowMemory; var abortStackOverflow=env.abortStackOverflow; var setTempRet0=env.setTempRet0; var getTempRet0=env.getTempRet0; var tempFloat = 0.0; // EMSCRIPTEN_START_FUNCS function stackAlloc(size) { size = size|0; var ret = 0; ret = STACKTOP; STACKTOP = (STACKTOP + size)|0; STACKTOP = (STACKTOP + 15)&-16; if ((STACKTOP|0) >= (STACK_MAX|0)) abortStackOverflow(size|0); return ret|0; } function stackSave() { return STACKTOP|0; } function stackRestore(top) { top = top|0; STACKTOP = top; } function establishStackSpace(stackBase, stackMax) { stackBase = stackBase|0; stackMax = stackMax|0; STACKTOP = stackBase; STACK_MAX = stackMax; } function setThrew(threw, value) { threw = threw|0; value = value|0; if ((__THREW__|0) == 0) { __THREW__ = threw; threwValue = value; } } function _get_start() { var label = 0, sp = 0; sp = STACKTOP; return ((gb + (0) | 0)|0); } function _get_end() { var $0 = 0, $1 = 0, label = 0, sp = 0; sp = STACKTOP; $0 = HEAP32[(gb + (16384) | 0)>>2]|0; $1 = ((gb + (0) | 0) + ($0<<2)|0); return ($1|0); } function _generate_array($0) { $0 = $0|0; var $1 = 0, $10 = 0, $11 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, label = 0, sp = 0; sp = STACKTOP; STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abortStackOverflow(16|0); $1 = $0; $2 = 0; while(1) { $3 = $2; $4 = $1; $5 = ($3|0)<($4|0); if (!($5)) { break; } $6 = $2; $7 = $2; $8 = ((gb + (0) | 0) + ($7<<2)|0); HEAP32[$8>>2] = $6; $9 = $2; $10 = (($9) + 1)|0; $2 = $10; } $11 = $1; HEAP32[(gb + (16384) | 0)>>2] = $11; STACKTOP = sp;return; } function runPostSets() { var temp = 0; } // EMSCRIPTEN_END_FUNCS return { runPostSets: runPostSets, establishStackSpace: establishStackSpace, stackSave: stackSave, stackRestore: stackRestore, _get_end: _get_end, setThrew: setThrew, stackAlloc: stackAlloc, _generate_array: _generate_array, _get_start: _get_start }; }) // EMSCRIPTEN_END_ASM (Module.asmGlobalArg, Module.asmLibraryArg, buffer); var real_setThrew = asm["setThrew"]; asm["setThrew"] = function() { assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); return real_setThrew.apply(null, arguments); }; var real__generate_array = asm["_generate_array"]; asm["_generate_array"] = function() { assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); return real__generate_array.apply(null, arguments); }; var real__get_start = asm["_get_start"]; asm["_get_start"] = function() { assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); return real__get_start.apply(null, arguments); }; var real__get_end = asm["_get_end"]; asm["_get_end"] = function() { assert(runtimeInitialized, 'you need to wait for the runtime to be ready (e.g. wait for main() to be called)'); assert(!runtimeExited, 'the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)'); return real__get_end.apply(null, arguments); }; var setThrew = Module["setThrew"] = asm["setThrew"]; var _generate_array = Module["_generate_array"] = asm["_generate_array"]; var runPostSets = Module["runPostSets"] = asm["runPostSets"]; var _get_start = Module["_get_start"] = asm["_get_start"]; var _get_end = Module["_get_end"] = asm["_get_end"]; var NAMED_GLOBALS = { }; for (var named in NAMED_GLOBALS) { Module['_' + named] = gb + NAMED_GLOBALS[named]; } Module['NAMED_GLOBALS'] = NAMED_GLOBALS; ; Runtime.registerFunctions([], Module); // === Auto-generated postamble setup entry stuff === __ATPRERUN__.push(runPostSets); if (runtimeInitialized) { // dlopen case: we are being loaded after the system is fully initialized, so just run our prerun and atinit stuff now callRuntimeCallbacks(__ATPRERUN__); callRuntimeCallbacks(__ATINIT__); } // otherwise, general dynamic linking case: stuff we added to prerun and init will be executed with the rest of the system as it loads // {{MODULE_ADDITIONS}} return Module; });
往后用Binaryen這一步總是不成功。而后經過我不斷探索,發現了另一個神器:WebAssembly Explorer。他是一個在線的wasm編譯器,能夠很完美編譯出我們想要的wasm,而且沒有膠水代碼。
這里C代碼的環境最好選擇C99,這樣編譯出來的函數名跟你C模塊中的函數名是一致的,否則會有一些不一樣。
然后我們可以在頁面使用這個模塊:
<html> <head> <meta charset="UTF-8"> <title>Game of Life</title> </head> <body> <canvas id="game"></canvas> <script> // 通過fetch獲取wasm模塊 fetch('./test.wasm').then(function (response) { return response.arrayBuffer(); }).then(function (bytes) { // 初始化內存,1個單位代表64kb=65536byte var memory = new WebAssembly.Memory({initial: 1, maximum: 1}); WebAssembly.instantiate(bytes, { env: { // memory 實例 // memory: memory, // table 實例 table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' }), // 以下都是編譯生成的wasm所需要的變量,不需要可以直接傳0 abortStackOverflow: function () {}, DYNAMICTOP_PTR: 0, tempDoublePtr: 0, ABORT: 0, STACKTOP: 0, STACK_MAX: 0, gb: 0, fb: 0, memoryBase: 0, tableBase: 0 }, global: { NaN: NaN, Infinity: Infinity } }).then(function (results) { // 得到編譯后的wasm模塊實例 var module = results.instance.exports; // 調用模塊函數,生成從0到99的數組 module.generate_array(100); // 通過slice偏移量得到最終的生成的數組 var generatedArray = new Int32Array(module.memory.buffer).slice(module.get_start() >> 2, module.get_end() >> 2); console.log(generatedArray); }); }); </script> </body> </html>
然后呢我們在控制台得到如下結果:
這里需要注意的是,如果你按照百度文章里面的代碼來寫,你是跑不出這個結果,為什呢,我覺得百度這位同學估計也沒理解好WebAssembly的memory這個概念。
var generatedArray = new Int32Array(memory.buffer).slice(module._get_start() >> 2, module._get_end() >> 2);
這行是它的文章中的代碼,這里使用的memory.buffer根本不是wasm這個模塊module的內存。
這里應當使用的是module.memory.buffer!!!!!!!!!!!!!!!
這里應當使用的是module.memory.buffer!!!!!!!!!!!!!!!
這里應當使用的是module.memory.buffer!!!!!!!!!!!!!!!
因為這個問題,把我折騰到兩點,果然不能全信百度。但終歸來講還是要謝謝這篇文章,解了我對WebAssembly的一個疑惑。
WebAssembly的兼容性
總的來說這個新特性的發展前景是比較好的,頭一次由所有的瀏覽器廠商都打成一致意見。而且未來還會讓我們在里面操作DOM。目前階段來說,看下圖嘍
參考資料
下面就是我搜集的跟WebAssembly相關的一些比較好的資料(作為一個技術我最煩那些只講技術歷史不講實際內容的文章)