5.3 asm.js
asm.js(http://asmjs.org )这个标签是指 JavaScript 语言中可以高度优化的一个子集。通过小心避免某些难以优化的机制和模式(垃圾收集、类型强制转换,等等),asm.js 风格的代码可以被 JavaScript 引擎识别并进行特别激进的底层优化。
和本章前面讨论的其他程序性能机制不同,asm.js 并不是 JavaScript 语言规范需要采纳的某种东西。虽然 asm.js 规范的确存在(http://asmjs,org/spec/latest/ ),但它主要是用来追踪一系列达成一致的备选优化方案而不是对 JavaScript 引擎的一组要求。
目前还没有提出任何新的语法。事实上,asm.js 提出了一些识别满足 asm.js 规则的现存标准 JavaScript 语法的方法,并让引擎据此实现它们自己的优化。
浏览器提供者之间在关于程序中应如何激活 asm.js 这一点上有过一些分歧。早期版本的 asm.js 实验需要一个 "use asm" ;pragma(类似于严格模式的 "use strict"; )帮助提醒 JavaScript 引擎寻找 asm.js 优化机会。另外一些人认为,asm.js 应该就是一个启发式的集合,引擎应该能够自动识别,无需开发者做任何额外的事情。这意味着,从理论上说,现有的程序可以从 asm.js 风格的优化得益而无需特意做什么。
5.3.1 如何使用 asm.js 优化
关于 asm.js 优化,首先要理解的是类型和强制类型转换(参见本书的“类型和语法”部分)。如果 JavaScript 引擎需要跟踪一个变量在各种各样的运算之间的多个不同类型的值,才能按需处理类型之间的强制类型转换,那么这大量的额外工作会使得程序优化无法达到最优。
为了解释明了,我们在这里将使用 asm.js 风格代码,但你要清楚,通常并不需要手工编写这样的代码。asm.js 通常是其他工具的编译目标,比如 Emscripten(https://github.com/kripken/emscripten/wiki )。当然,你也可以自己编写 asm.js 代码,但一般来说,这想法并不好,因为这是非常耗时且容易出错的过程。尽管如此,可能还是会有一些情况需要你修改代码,以便于 asm.js 优化。
还有一些技巧可以用来向支持 asm.js 的 JavaScript 引擎暗示变量和运算想要的类型是什么,使它可以省略这些类型转换跟踪步骤。
比如:
var a = 42; // .. var b = a;
在这个程序中,赋值 b = a 留下了变量类型二义性的后门。但它也可以换一种方式,写成这样:
var a = 42; // .. var b = a | 0;
此处我们使用了与 0 的 |(二进制或 )运算,除了确保这个值是 32 位整型之外,对于值没有任何效果。这样的代码在一般的 JavaScript 引擎上都可以正常工作。而对支持 asm.js 的 JavaScript 引擎来说,这段代码就发出这样的信号,b 应该总是被当作 32 位整型来处理,这样就可以省略强制类型转换追踪。
类似地,可以这样把两个变量的加运算限制为更高效的整型加运算(而不是浮点型):
(a + b) | 0
另一方面,支持 asm.js 的 JavaScript 引擎可以看到这个提示并推导出这里的 + 运算应该是 32 位整型加,因为不管怎样,整个表达式的结果都会自动规范为 32 位整型。
5.3.2 asm.js 模块
对 JavaScript 性能影响最大的因素是内存分配、垃圾收集和作用域访问。asm.js 对这些问题提出的一个解决方案就是,声明一个更正式的 asm.js“模块”,不要和 ES6 模块混淆。请参考本系列《你不知道的 JavaScript(下卷)》的“ES6 & Beyond”部分。
对一个 asm.js 模块来说,你需要明确地导入一个严格规范的命名空间——规范将之称为 stdlib ,因为它应该代表所需的标准库——以导入必要的符号,而不是通过词法作用域使用全局的那些符号。基本上,window 对象就是一个 asm.js 模块可以接受的 stdlib 对象,但是,你能够而且可能也需要构造一个更加严格的。
你还需要声明一个堆 (heap)并将其传入。这个术语用于表示内存中一块保留的位置,变量可以直接使用而不需要额外的内存请求或释放之前使用的内存。这样,asm.js 模块就不需要任何可能导致内存扰动的动作了,只需使用预先保留的空间即可。
一个堆就像是一个带类型的 ArrayBuffer ,比如:
var heap = new ArrayBuffer( 0x10000 ); // 64k堆
由于使用这个预留的 64k 二进制空间,asm.js 模块可以在这个缓冲区存储和获取值,不需要付出任何内存分配和垃圾收集的代价。举例来说,可以在模块内部使用堆缓冲区备份一个 64 位浮点值数组,就像这样:
var arr = new Float64Array( heap );
用一个简单快捷的 asm.js 风格模块例子来展示这些细节是如何结合到一起的。我们定义了一个 foo(..) 。它接收一个起始值(x )和终止值(y )整数构成一个范围,并计算这个范围内的值的所有相邻数的乘积,然后算出这些值的平均数:
function fooASM(stdlib,foreign,heap) { "use asm"; var arr = new stdlib.Int32Array( heap ); function foo(x,y) { x = x | 0; y = y | 0; var i = 0; var p = 0; var sum = 0; var count = ((y|0) - (x|0)) | 0; // 计算所有的内部相邻数乘积 for (i = x | 0; (i | 0) < (y | 0); p = (p + 8) | 0, i = (i + 1) | 0 ) { // 存储结果 arr[ p >> 3 ] = (i * (i + 1)) | 0; } // 计算所有中间值的平均数 for (i = 0, p = 0; (i | 0) < (count | 0); p = (p + 8) | 0, i = (i + 1) | 0 ) { sum = (sum + arr[ p >> 3 ]) | 0; } return +(sum / count); } return { foo: foo }; } var heap = new ArrayBuffer( 0x1000 ); var foo = fooASM( window, null, heap ).foo; foo( 10, 20 ); // 233
出于展示的目的,这个 asm.js 例子是手写的,所以它并不能代表由目标为 asm.js 的编译工具产生的同样功能的代码。但是,它确实显示了 asm.js 代码的典型特性,特别是类型提示以及堆缓冲区在存储临时变量上的使用。
第一个对 fooASM(..) 的调用建立了带堆分配的 asm.js 模块。结果是一个 foo(..) 函数,我们可以按照需要调用任意多次。这些 foo(..) 调用应该被支持 asm.js 的 JavaScript 引擎专门优化。很重要的一点是,前面的代码完全是标准 JavaScript,在非 asm.js 引擎中也能正常工作(没有特殊优化)。
显然,使 asm.js 代码如此高度可优化的那些限制的特性显著降低了这类代码的使用范围。asm.js 并不是对任意程序都适用的通用优化手段。它的目标是对特定的任务处理提供一种优化方法,比如数学运算(如游戏中的图形处理)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论