之前的几篇文章分别介绍了WebWorkers、Typed Array和ParallelArray,最后,我们再来介绍一下与性能相关的标准: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语言不仅是弱类型的,而且数值类型只有一种-Number
,Number
类型的数据采用双精度64位格式的IEEE 754值表示。如果用JS模拟C/C++类型的数值计算,就要模拟各种类型数据计算时的溢出效果。我们通过下面这个简单的C程序来看看如何用JS来模拟这样的计算。
2 | char yInt8 = xInt8 + 1; // 溢出:yInt8 == (char) -128 |
3 | cahr zInt8 = xInt8 / 2; // 舍入:zInt8 == (char) 63 |
上面这段代码通过JS模拟后的代码如下:
2 | var $add = (xInt8 + 1) | 0; // (2) |
3 | var yInt8 = ($add << 24) >> 24; // (3) |
4 | var $div = ((yInt8 | 0) / 2) & -1; // (4) |
5 | var zInt8 = ($div << 24) >> 24; // (5) |
- 8位整型变量127,相当于
char xInt8 = 127;
X|0
让计算结果成为32位整数,此时$add == 128
- 先左移24位再右移24位,让第8位成为32位整数的符号位,来模拟8位整数计算,此时
yInt8 == -128
- JS中
127/2
结果是浮点数63.5,利用X&-1
将结果转化成32位整数63(-1的补码表示就是0xFFFFFFFF) - 用上面(3)相同的方法,把结果转换成8位整数
大家看到,利用一些位移和逻辑运算可以模拟C/C++语言中的数据计算,Emscripten就利用这个方法将C代码转换成JS代码。大家可能还记得前篇文章介绍过的Typed Array,对Typed Array元素赋值则会自动进行相应的溢出和舍入处理,因此,利用Typed Array还可以改写成以下的代码:
1 | var HEAP8 = new Int8Array(STACK_SIZE); // 构造一个Int8Array数组 |
2 | HEAP8[0] = 127; // char xInt8 = 127 |
3 | HEAP8[1] = HEAP[0] + 1; // char yInt8 = xInt8 + 1,此时yInt8是-128 |
4 | 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转换后的代码执行效率的。我们来看一个例子:
1 | intValue = f1() | 0; // 利用或运算(|)标记函数f1返回值为int32整数 |
2 | floatValue = +f2(); // 用加号(+)标记函数f2返回值为双精度型浮点 |
同时,asm.js还规定了一个特殊的语法格式,下面这段代码是一个最简单的asm.js示例(代码来自:http://asmjs.org/spec/latest/):
1 | function MyAsmModule(stdlib, foreign, heap) { |
2 | "use asm" ; // "use asm"来告诉JS引擎这个函数采用asm.js编译器解析执行 |
9 | 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引 擎解析执行(见下图)。
图片来自:http://asmjs.org/spec/latest/性能
JS一直以来被人诟病的一个方面就是它的性能,得益于这些年来浏览器之间的竞争,让JS的性能大大提升,Google的V8、Mozilla的 SpiderMonkey以及微软的Chakra在性能方面都已经相当不错,而asm.js进一步提升到相对本地代码2倍慢的性能(如下图)。这些测试用 例使用Emscripten转换而来,Emscripten已经可以直接生成asm.js代码。
数据来自:http://ejohn.org/blog/asmjs-javascript-compile-target/Demo
参考
- asm.js specification: http://asmjs.org/spec/latest/
- IEEE 754: https://en.wikipedia.org/wiki/IEEE_floating_point