本文探究 asm.js 为什么比普通js快,是否可以用在小程序上。
asm.js为什么快
- 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引擎解析执行。

从图中可以看到:
编译时,如果能通过类型检查(type check),就可以生成字节码,否则直接进入普通js处理流程。
asm模块和标准库链接(link)后,在运行时检查(runtime checks)如果符合规范,编译成字节码执行;如果不符合规范,把代码扔给普通js处理流程,普通js会用解释器进行处理后再执行。
- 没有垃圾回收
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模块。

