现在的 JavaScript 代码要进行性能优化,通常使用一些常规手段,如:延迟执行、预处理、setTimeout 等异步方式避免处理主线程,高大上一点的会使用 WebWorker。即使对于 WebWorker 也仅仅是解决了阻塞主线程的问题,但是对于 JavaScript 计算性能慢的问题并没有解决。这里对一些需要密集计算的场景我给大家推荐一个神器——WebAssembly。在目前阶段,WebAssembly 适合大量密集计算、并且无需频繁与 JavaScript 及 DOM 进行数据通讯的场景。比如游戏渲染引擎、物理引擎、图像音频视频处理编辑、加密算法等
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 的结合性最好。
在所有工具链之前,需要安装下面几个工具:
然后下载编译 Emscripten,这玩意儿需要 FQ,然后特别大,得慢慢等个半小时差不多,然后千万别按照它官网的介绍来安装,要按照 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,但是在一些需要极大的计算能力的场景,比如 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 的一个疑惑。
总的来说这个新特性的发展前景是比较好的,头一次由所有的浏览器厂商都打成一致意见。而且未来还会让我们在里面操作 DOM。目前阶段来说,看下图喽
下面就是我搜集的跟 WebAssembly 相关的一些比较好的资料(作为一个技术我最烦那些只讲技术历史不讲实际内容的文章)
来源: http://www.cnblogs.com/dojo-lzz/p/8053250.html