asm.js

“asm.js”(http://asmjs.org/ )是可以被高度优化的JavaScript语言子集的标志。通过小心地回避那些特定的很难优化的(垃圾回收,强制转换,等等)机制和模式,asm.js风格的代码可以被JS引擎识别,而且用主动地底层优化进行特殊的处理。

与本章中讨论的其他性能优化机制不同的是,asm.js没必须要是必须被JS语言规范所采纳的东西。确实有一个asm.js规范(http://asmjs.org/spec/latest/ ),但它主要是追踪一组关于优化的候选对象的推论,而不是JS引擎的需求。

目前还没有新的语法被提案。取而代之的是,ams.js建议了一些方法,用来识别那些符合ams.js规则的既存标准JS语法,并且让引擎相应地实现它们自己的优化功能。

关于ams.js应当如何在程序中活动的问题,在浏览器生产商之间存在一些争议。早期版本的asm.js实验中,要求一个"use asm";编译附注(与strict模式的"use strict";类似)来帮助JS引擎来寻找asm.js优化的机会和提示。另一些人则断言asm.js应当只是一组启发式算法,让引擎自动地识别而不用作者做任何额外的事情,这意味着理论上既存的程序可以在不用做任何特殊的事情的情况下从asm.js优化中获益。

如何使用 asm.js 进行优化

关于asm.js需要理解的第一件事情是类型和强制转换。如果JS引擎不得不在变量的操作期间一直追踪一个变量内的值的类型,以便于在必要时它可以处理强制转换,那么就会有许多额外的工作使程序处于次优化状态。

注意: 为了说明的目的,我们将在这里使用ams.js风格的代码,但要意识到的是你手写这些代码的情况不是很常见。asm.js的本意更多的是作为其他工具的编译目标,比如Emscripten(https://github.com/kripken/emscripten/wiki )。当然你写自己的asm.js代码也是可能的,但是这通常不是一个好主意,因为那样的代码非常底层,而这意味着它会非常耗时而且易错。尽管如此,也会有情况使你想要为了ams.js优化的目的手动调整代码。

这里有一些“技巧”,你可以使用它们来提示支持asm.js的JS引擎变量/操作预期的类型是什么,以便于它可以跳过那些强制转换追踪的步骤。

举个例子:

  1. var a = 42;
  2. // ..
  3. var b = a;

在这个程序中,赋值b = a在变量中留下了类型分歧的问题。然而,它可以写成这样:

  1. var a = 42;
  2. // ..
  3. var b = a | 0;

这里,我们与值0一起使用了|(“二进制或”),虽然它对值没有任何影响,但它确保这个值是一个32位整数。这段代码在普通的JS引擎中可以工作,但是当它运行在支持asm.js的JS引擎上时,它 可以 表示b应当总是被作为32位整数来对待,所以强制转换追踪可以被跳过。

类似地,两个变量之间的加法操作可以被限定为性能更好的整数加法(而不是浮点数):

  1. (a + b) | 0

再一次,支持asm.js的JS引擎可以看到这个提示,并推断+操作应当是一个32位整数加法,因为不论怎样整个表达式的最终结果都将自动是32位整数。

asm.js 模块

在JS中最托性能后腿的东西之一是关于内存分配,垃圾回收,与作用域访问。asm.js对于这些问题建一个的一个方法是,声明一个更加正式的asm.js“模块”——不要和ES6模块搞混;参见本系列的 ES6与未来

对于一个asm.js模块,你需要明确传入一个被严格遵循的名称空间——在规范中以stdlib引用,因为它应当代表需要的标准库——来引入需要的符号,而不是通过词法作用域来使用全局对象。在最基本的情况下,window对象就是一个可接受的用于asm.js模块的stdlib对象,但是你可能应该构建一个更加被严格限制的对象。

你还必须定义一个“堆(heap)”——这只是一个别致的词汇,它表示在内存中被保留的位置,变量不必要求内存分配或释放已使用内存就可以使用——并将它传入,这样asm.js模块就不必做任何导致内存流失的的事情;它可以使用提前保留的空间。

一个“堆”就像一个有类型的ArrayBuffer,比如:

  1. var heap = new ArrayBuffer( 0x10000 ); // 64k 的堆

使用这个提前保留的64k的二进制空间,一个asm.js模块可以在这个缓冲区中存储或读取值,而不受任何内存分配与垃圾回收的性能损耗。比如,heap缓冲区可以在模块内部用于备份一个64位浮点数值的数组,像这样:

  1. var arr = new Float64Array( heap );

好了,让我制作一个asm.js风格模块的快速,愚蠢的例子来描述这些东西是如何联系在一起的。我们将定义一个foo(..),它为一个范围接收一个开始位置(x)和一个终止位置(y),并且计算这个范围内所有相邻的数字的积,然后最终计算这些值的平均值:

  1. function fooASM(stdlib,foreign,heap) {
  2. "use asm";
  3. var arr = new stdlib.Int32Array( heap );
  4. function foo(x,y) {
  5. x = x | 0;
  6. y = y | 0;
  7. var i = 0;
  8. var p = 0;
  9. var sum = 0;
  10. var count = ((y|0) - (x|0)) | 0;
  11. // 计算范围内所有相邻的数字的积
  12. for (i = x | 0;
  13. (i | 0) < (y | 0);
  14. p = (p + 8) | 0, i = (i + 1) | 0
  15. ) {
  16. // 存储结果
  17. arr[ p >> 3 ] = (i * (i + 1)) | 0;
  18. }
  19. // 计算所有中间值的平均值
  20. for (i = 0, p = 0;
  21. (i | 0) < (count | 0);
  22. p = (p + 8) | 0, i = (i + 1) | 0
  23. ) {
  24. sum = (sum + arr[ p >> 3 ]) | 0;
  25. }
  26. return +(sum / count);
  27. }
  28. return {
  29. foo: foo
  30. };
  31. }
  32. var heap = new ArrayBuffer( 0x1000 );
  33. var foo = fooASM( window, null, heap ).foo;
  34. foo( 10, 20 ); // 233

注意: 这个asm.js例子是为了演示的目的手动编写的,所以它与那些支持asm.js的编译工具生产的代码的表现不同。但是它展示了asm.js代码的典型性质,特别是类型提示与为了临时变量存储而使用heap缓冲。

第一个fooASM(..)调用用它的heap分配区建立了我们的asm.js模块。结果是一个我们可以调用任意多次的foo(..)函数。这些调用应当会被支持asm.js的JS引擎特别优化。重要的是,前面的代码完全是标准JS,而且会在非asm.js引擎中工作的很好(但没有特别优化)。

很明显,使asm.js代码可优化的各种限制降低了广泛使用这种代码的可能性。对于任意给出的JS程序,asm.js没有必要为成为一个一般化的优化集合。相反,它的本意是提供针对一种处理特定任务——如密集数学操作(那些用于游戏中图形处理的)——的优化方法。