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

之前的几篇文章分别介绍了WebWorkersTyped ArrayParallelArray,最后,我们再来介绍一下与性能相关的标准:asm.js。

asm.js

asm.js是由Mozilla提出的一个基于JS的语法标准,主要是为了解决JS引擎的执行效率问题,尤其是使用Emscripten从C/C++语言编译成JS的程序的效率,目前只有Mozilla的Firefox Nightly中支持。

Emscripten

Emscripten是Mozilla的一个实验性项目,目的是把C/C++开发的应用编译成JS或HTML5的应用,编译过程中需要首先把C/C++程序编译成LLVM的中间代码,然后再转换成JS代码,这样做的主要原因是可以很好地复用现有的针对LLVM的优化。

C/C++是一种强类型的语言,这很好地对应了不同字长的CPU指令,每一种数据类型都有固定的上下限,一旦计算超出这个上下限就会产生溢出,比如char类型的取值范围是[-128,127],而(char)(127 + 1)这个计算就会溢出,得到(char)-128

JS语言不仅是弱类型的,而且数值类型只有一种-NumberNumber类型的数据采用双精度64位格式的IEEE 754值表示。如果用JS模拟C/C++类型的数值计算,就要模拟各种类型数据计算时的溢出效果。我们通过下面这个简单的C程序来看看如何用JS来模拟这样的计算。

char xInt8 = 127;
char yInt8 = xInt8 + 1; // 溢出:yInt8 == (char) -128
cahr zInt8 = xInt8 / 2; // 舍入:zInt8 == (char) 63 

上面这段代码通过JS模拟后的代码如下:

var xInt8 = 127; // (1)
var $add = (xInt8 + 1) | 0; // (2)
var yInt8 = ($add << 24) >> 24; // (3)
var $div = ((yInt8 | 0) / 2) & -1; // (4)
var zInt8 = ($div << 24) >> 24; // (5) 
  1. 8位整型变量127,相当于char xInt8 = 127;
  2. X|0让计算结果成为32位整数,此时$add == 128
  3. 先左移24位再右移24位,让第8位成为32位整数的符号位,来模拟8位整数计算,此时yInt8 == -128
  4. JS中127/2结果是浮点数63.5,利用X&-1将结果转化成32位整数63(-1的补码表示就是0xFFFFFFFF)
  5. 用上面(3)相同的方法,把结果转换成8位整数

大家看到,利用一些位移和逻辑运算可以模拟C/C++语言中的数据计算,Emscripten就利用这个方法将C代码转换成JS代码。大家可能还记得前篇文章介绍过的Typed Array,对Typed Array元素赋值则会自动进行相应的溢出和舍入处理,因此,利用Typed Array还可以改写成以下的代码:

var HEAP8 = new Int8Array(STACK_SIZE); // 构造一个Int8Array数组
HEAP8[0] = 127; // char xInt8 = 127
HEAP8[1] = HEAP[0] + 1; // char yInt8 = xInt8 + 1,此时yInt8是-128
HEAP8[2] = HEAP[1] / 2; // char zInt8 = xInt8 / 2 

在EmScripten中,Typed Array用来模拟C/C++中的堆栈以及指针的访问。

语法

asm.js不是一种新的语言,而是JS语法的一个子集,也就是说所有用asm.js写的程序都是合法的JS程序,asm.js与JS语言的关系有点类似C与C++的关系。因此,不支持asm.js的浏览器或JS引擎也可以无误地执行asm.js的代码。

asm.js顾名思义是作为JS的汇编语言来设计的,它的语法手写起来非常困难,且难以阅读。首先,asm.js的语法利用了一些标注让JS的变量成为强类型的,这些标注与Emscripten生成的代码如出一辙,实际上asm.js的产生就是为了提高Emscripten转换后的代码执行效率的。我们来看一个例子:

intValue = f1() | 0; // 利用或运算(|)标记函数f1返回值为int32整数
floatValue = +f2(); // 用加号(+)标记函数f2返回值为双精度型浮点 

同时,asm.js还规定了一个特殊的语法格式,下面这段代码是一个最简单的asm.js示例(代码来自:http://asmjs.org/spec/latest/):

function MyAsmModule(stdlib, foreign, heap) {
"use asm"; // "use asm"来告诉JS引擎这个函数采用asm.js编译器解析执行
// module body...
return { // 返回向外暴露的函数接口
 export1: f1,
 export2: f2,
 // ... };
}
var result = MyAsmModule({}, {}, null).export1(); // 调用函数export1 

在这个例子中,参数stdlib、foreign、heap由外部传入,表示: 1. stdlib:有限的标准库函数,主要是一些数学函数,对应Math对象上的方法 2. foreign:foreign function interface (FFI)外部JS函数访问接口 3. heap:传入一个ArrayBuffer对象,作为asm.js的堆

编译和运行

由于asm.js相当于支持了强类型,因此可以直接对应编译成机器指令执行。asm.js的代码采用另外一套AOT(Ahead Of Time)编译器,将asm.js代码预先编译成机器指令,在编译过程或运行过程中,一旦发现语法错误或违反类型标记的情况出现,便重新将代码交予JS引擎解析执行(见下图)。

Linking图片来自:http://asmjs.org/spec/latest/

性能

JS一直以来被人诟病的一个方面就是它的性能,得益于这些年来浏览器之间的竞争,让JS的性能大大提升,Google的V8、Mozilla的SpiderMonkey以及微软的Chakra在性能方面都已经相当不错,而asm.js进一步提升到相对本地代码2倍慢的性能(如下图)。这些测试用例使用Emscripten转换而来,Emscripten已经可以直接生成asm.js代码。 Performance数据来自:http://ejohn.org/blog/asmjs-javascript-compile-target/

Demo

参考

  1. asm.js specification: http://asmjs.org/spec/latest/
  2. IEEE 754: https://en.wikipedia.org/wiki/IEEE_floating_point