asm.js为什么快?小程序能否使用asm.js?

本文探究 asm.js 为什么比普通js快,是否可以用在小程序上。

asm.js为什么快

  1. asm的变量都是静态类型

并且只有两种类型:

32位有符号整数 int32

64位浮点数 float64

这两种类型都是占据固定长度内存,因此,可以将asm直接静态编译到字节码


    var a = 1;  // 直接声明了一个整数 int32

    var x = a|0  // 不是直接声明,但是 |0 为 x 限制了变量类型为int32

    var y = +a  // 不是直接声明,但是 +为 y限制了变量类型为float64

函数也一样:


    function add(x, y) {
        x = x | 0;
        y = y | 0;
        return (x + y) | 0;  // 返回值限制为 int32
    }

可以看到,这些代码当成普通js来运行也没有问题

那涉及到引用或指针呢?

如果涉及到指针,也是一样处理。


size_t strlen(char *ptr) {
  char *curr = ptr;
  while (*curr != 0) {
    curr++;
  }
  return (curr - ptr);
}

上面的代码编译成 asm.js,就是下面这样。


function strlen(ptr) {
  ptr = ptr|0;
  var curr = 0;
  curr = ptr;
  while (MEM8[curr]|0 != 0) {   // 取指针 MEM8[i32]
    curr = (curr + 1)|0;
  }
  return (curr - ptr)|0;
}

指针也被当成了一个整数,指针指向哪里呢,我们给asm模块传出一个arraybuffer,作为内存来使用,堆、栈、都在这上面创建,对于上面这段代码来说就是MEM8

如果不按照asm的规范来写会怎么样

由于asm.js相当于支持了强类型,因此可以直接对应编译成机器指令执行。

因此asm.js的代码采用一套AOT(Ahead Of Time)编译器,将asm.js代码预先编译成机器指令。

在编译过程或运行过程中,一旦发现语法错误或违反类型标记的情况出现,便重新将代码交予JS引擎解析执行。

asm.js处理流程
从图中可以看到:

编译时,如果能通过类型检查(type check),就可以生成字节码,否则直接进入普通js处理流程。

asm模块和标准库链接(link)后,在运行时检查(runtime checks)如果符合规范,编译成字节码执行;如果不符合规范,把代码扔给普通js处理流程,普通js会用解释器进行处理后再执行。

  1. 没有垃圾回收

asm.js所用的堆、栈,都是自己用arrayBuffer模拟出来的,arrayBuffer可以看做底层内存。

asm.js并不提供垃圾回收机制,因此这些内存是由用户自己控制的。

js引擎的垃圾回收机制本身依然存在,但是不会对这部分内存造成影响。因此可以认为,asm模块没有垃圾回收。

根据以上两点,asm.js可以比普通js更快。但是,不适合手写,最好是从手动控制内存的静态语言编译过来,比如c语言。


asm.js如何和外部js代码交互

运用编译工具编译出asm模块时,一般编译工具已经提供了交互接口,并提供了普通js的调用api,所以我们一般只要调用api就好了。

从原理来讲,主要是以下两种方式:

(1)部分js标准库,这些库被预先静态编译成字节码配合asm.js使用

(2)ArrayBuffer,相当于底层内存

实例(来自官网):


function GeometricMean(stdlib, foreign, buffer) {  // stdlib  代表标准库 buffer 用来和外部做数据交互
  "use asm";
  var exp = stdlib.Math.exp;  // 使用标准库方法
  var log = stdlib.Math.log;    // 使用标准库方法
  var values = new stdlib.Float64Array(buffer);  // 使用标准库对象
  function logSum(start, end) {
    start = start|0;
    end = end|0;
    var sum = 0.0, p = 0, q = 0;
    // asm.js forces byte addressing of the heap by requiring shifting by 3
    for (p = start << 3, q = end << 3; (p|0) < (q|0); p = (p + 8)|0) {
      sum = sum + +log(values[p>>3]);
    }

    return +sum;
  }

  function geometricMean(start, end) {
    start = start|0;
    end = end|0;

    return +exp(+logSum(start, end) / +((end - start)|0));
  }
  return { geometricMean: geometricMean };
}
var heap = new ArrayBuffer(0x10000);          // 定义一个和asm模块交互的数据
var fast = GeometricMean(window, null, heap); // 调用asm模块

上面这段实例,基本展示了一个asm.js模块的基本结构,注意添加 “use asm”

// asm模块结构示例
var asm = ( function (global, env, buffer) {
    "use asm";
    function f1(x, y) {
        x = x | 0;
        y = y | 0;
        return (x + y) | 0; 
    }
    return {
        func1:f1
    }
})


WebAssembly & asm.js

WebAssembly和asm.js的功能基本一致,但是转出来的代码不一样:asm.js 是文本,WebAssembly是二进制字节码,因此运行速度更快、体积更小

在这里插入图片描述
WebAssembly在下载和编译之后,就生成了字节码,对照asm.js处理流程的示意图来看,不需要像asm.js那样还需要经过 type check 和 runtime check 的过程,直接就到了 executable 的 阶段,省去了大量的流程,因此性能也有了显著的提升。

但是WebAssembly的编译需要浏览器支持其API,WebAssembly.compile()

因此,WebAssembly有兼容性问题,对于不兼容的浏览器需要手动降级为asm.js

而asm.js 不会存在兼容性问题,自动降级为普通js执行

兼容性

因为本身就是 js 的子集,完全符合js语法,因此完全没有兼容性的问题,对于不支持 asm.js 的 浏览器,只是当成普通 js 来运行。

这是asm.js最大的优点。

对小程序来说,asm.js可以直接使用(怎么用js检测浏览器是否支持?目前没有找到方法)。

对于WebAssembly,小程序没有明确支持,在真机上android的可用。我们可以先使用asm.js,同时关注小程序对WebAssembly的支持。

WebAssembly可以让开发者热更新小程序,可能会限制使用。比如不允许远程加载WebAssembly模块,只能加载小程序包内的WebAssembly模块。

asm.js兼容性
wasm兼容性

参考

asm.js官网

asm.js 和 Emscripten 入门教程

HTML5标准与性能之四:asm.js

caniuse-asm


版权声明:本文为madlaxnoir原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。