Asm.js: Javascript的编译目标
正如许多开发者一样,我也为Asm.js的前景而感到兴奋不已。最近的新闻——Asm.js正 在被Firefox支持——引起了我的兴趣。同样感兴趣的还有Mozilla和Epic声明(mirror)他们已经为Asm.js而支持Unreal Engine 3——并且运行十分良好。
获得一个C++游戏引擎运行Javascript,并使用WebGL来渲染,这是一个重大的突破,这个突破很大程度上归功于Mozilla开发的工具链,才使得这一切变得可能。
由于Unreal Engine 3开始支持Asm.js,我浏览了来自Twitter,blogs和其他地方的回应,当一部分开发者表现出对这项制造奇迹的技术跃跃欲试时,我也看到许多 疑惑:Asm.js是一个插件吗?Asm.js能让我平时使用的Javascript运行的更快吗?它兼容所有浏览器吗?从这些回应中,我认为 Asm.js及其相关技术很重要,我将解释这些技术,好让开发者明白发生了什么以及他们将会如何从这些技术中获益。另外,为了写出这篇我对这项技术的概 览,我也请教了David Herman(Mozilla研究院的高级研究员)一大堆关于Asm.js和如何梳理这些知识的问题。
什么是 Asm.js?
为了理解 Asm.js 及其适用与浏览器的所在,你需要知道它的由来以及它存在的意义。
Asm.js 来自于 JavaScript 应用的一个新领域: 编译成JavaScript的C/C++应用. 它是 JavaScript 应用的一个全新流派,由 Mozilla 的 Emscripten项目催生而来。
Emscripten 将 C/C++ 代码传入 LLVM, 并将 LLVM生成的字节码转换成 JavaScript (具体的, Asm.js, 是 JavaScript 的一个子集).
如果被编译成的 Asm.js 的代码做了一些渲染工作,那么它几乎总是由WebGL来处理的 (并且由 OpenGL 来渲染). 这样技术上就利用了JavaScript 和浏览器的好处,但几乎完全避开了页面中Javascript使用的实际的、常规的代码执行和渲染路径.
Asm.js是Javascript的一个子集,它深度的限制了其所能做的范围和所能操作的对象。这样做才能使得Asm.js的代码能够尽可能运行的快, 从而尽可能减少各种假定情况的出现,从而能够把Asmjs代码直接转变成为汇编代码。有一个特别值得注意的是 - Asmjs还只是Javascript - 不需要浏览器的插件或者是别的特性去运行它(虽然一个能够检测出并且优化Asmjs代码的浏览器当然是要快一些)。它是Javascript的一个特定的 子集,为性能优化而生,特别是为那些需要编译成为Javascript的应用程序来做优化。
最佳的理解Asmjs工作的方式,就是看看一些Asmjs-编译化的代码。让我们看看这个函数,这是从真实的Asmjs编译模块里面提取出来的函数(来自BananaBread demo)。我对代码格式做了调整,所以看起来更加合乎代码片段的阅读,原本它是一个高度压缩的JavaScript代码中的一个很大的计算机二进制对象。
- function Vb(d) {
- d = d | 0;
- var e = 0, f = 0, h = 0, j = 0, k = 0, l = 0, m = 0, n = 0,
- o = 0, p = 0, q = 0, r = 0, s = 0;
- e = i;
- i = i + 12 | 0;
- f = e | 0;
- h = d + 12 | 0;
- j = c[h >> 2] | 0;
- if ((j | 0) > 0) {
- c[h >> 2] = 0;
- k = 0
- } else {
- k = j
- }
- j = d + 24 | 0;
- if ((c[j >> 2] | 0) > 0) {
- c[j >> 2] = 0
- }
- l = d + 28 | 0;
- c[l >> 2] = 0;
- c[l + 4 >> 2] = 0;
- l = (c[1384465] | 0) + 3 | 0;
- do {
- if (l >>> 0 < 26) {
- if ((4980736 >>> (l >>> 0) & 1 | 0) == 0) {
- break
- }
- if ((c[1356579] | 0) > 0) {
- m = d + 4 | 0;
- n = 0;
- while (1) {
- o = c[(c[1356577] | 0) + (n << 2) >> 2] | 0;
- do {
- if (a[o + 22 | 0] << 24 >> 24 == 24) {
- if (!(Vp(d, o | 0) | 0)) {
- break
- }
- p = (c[m >> 2] | 0) + (((c[h >> 2] | 0) - 1 | 0) * 40 & -1) + 12 | 0;
- q = o + 28 | 0;
- c[p >> 2] = c[q >> 2] | 0;
- c[p + 4 >> 2] = c[q + 4 >> 2] | 0;
- c[p + 8 >> 2] = c[q + 8 >> 2] | 0;
- c[p + 12 >> 2] = c[q + 12 >> 2] | 0;
- c[p + 16 >> 2] = c[q + 16 >> 2] | 0;
- c[p + 20 >> 2] = c[q + 20 >> 2] | 0;
- c[p + 24 >> 2] = c[q + 24 >> 2] | 0
- }
- } while (0);
- o = n + 1 | 0;
- if ((o | 0) < (c[1356579] | 0)) {
- n = o
- } else {
- break
- }
- }
- r = c[h >> 2] | 0
- } else {
- r = k
- } if ((r | 0) == 0) {
- i = e;
- return
- }
- n = c[j >> 2] | 0;
- if ((n | 0) >= 1) {
- i = e;
- return
- }
- m = f | 0;
- o = f + 4 | 0;
- q = f + 8 | 0;
- p = n;
- while (1) {
- g[m >> 2] = 0.0;
- g[o >> 2] = 0.0;
- g[q >> 2] = 0.0;
- Vq(d, p, f, 0, -1e3);
- n = c[j >> 2] | 0;
- if ((n | 0) < 1) {
- p = n
- } else {
- break
- }
- }
- i = e;
- return
- }
- } while (0);
- if ((c[1356579] | 0) <= 0) {
- i = e;
- return
- }
- f = d + 16 | 0;
- r = 0;
- while (1) {
- k = c[(c[1356577] | 0) + (r << 2) >> 2] | 0;
- do {
- if (a[k + 22 | 0] << 24 >> 24 == 30) {
- h = b[k + 14 >> 1] | 0;
- if ((h - 1 & 65535) > 1) {
- break
- }
- l = c[j >> 2] | 0;
- p = (c[1384465] | 0) + 3 | 0;
- if (p >>> 0 < 26) {
- s = (2293760 >>> (p >>> 0) & 1 | 0) != 0 ? 0 : -1e3
- } else {
- s = -1e3
- } if (!(Vq(d, l, k | 0, h << 16 >> 16, s) | 0)) {
- break
- }
- g[(c[f >> 2] | 0) + (l * 112 & -1) + 56 >> 2] = +(b[k + 12 >> 1] << 16 >> 16 | 0);
- h = (c[f >> 2] | 0) + (l * 112 & -1) + 60 | 0;
- l = k + 28 | 0;
- c[h >> 2] = c[l >> 2] | 0;
- c[h + 4 >> 2] = c[l + 4 >> 2] | 0;
- c[h + 8 >> 2] = c[l + 8 >> 2] | 0;
- c[h + 12 >> 2] = c[l + 12 >> 2] | 0;
- c[h + 16 >> 2] = c[l + 16 >> 2] | 0;
- c[h + 20 >> 2] = c[l + 20 >> 2] | 0;
- c[h + 24 >> 2] = c[l + 24 >> 2] | 0
- }
- } while (0);
- k = r + 1 | 0;
- if ((k | 0) < (c[1356579] | 0)) {
- r = k
- } else {
- break
- }
- }
- i = e;
- return
- }
从技术上说,这是Javascript代码,但是我们已经看到这段代码一点都不像大多我们正常看到的操作DOM的Javascript。通过看这段代码,我们可以发现几件事:
- 这段特定的代码只能处理数值。事实上这是所有Asm.js代码的情形。Asm.js只能处理被挑出的几种不同的数值类型,而没有提供其他的数据类型(包括字符串,布尔型和对象)。
- 所有外部数据在一个称为堆的对象中存储并被引用。堆在本质上是一个大数组(应当是一个在性能上高度优化的类型化数组)。所有的数据在这个数组中存储——有效的替代了全局变量,结构体,闭包和其他形式的数据存储。
- 当访问和赋值变量时,结果被统一的强制转换成一种特定类型。例如f = e | 0;给变量f赋值e,但它也确保了结果的类型是一个整数(|0把值转换成整数,确保了这点)。这也发生在浮点型上——注意0.0和g[...] = +(...)的使用.
- 看看从数据结构中写入读出的值,用c表示的数据结构好像是一个Int32Array(存储32位整数,因为这些值总是通过|0被转换成或者转换自一个整数),g好像是一个Float32Array(存储32位浮点数,因为这些值总是通过包裹上+(...)转换成浮点数)。
这么做以后,结果就是高度优化,并且可以直接从Asm.js语法转换成汇编,而不必像常常要对Javascript做的那样解释它。它有效地削减了使像Javascript之类的的动态语言缓慢的东西:例如需要垃圾收集器和动态类型。
作为一个更容易理解的Asm.js代码示例,我们来看看一个Asm.js规范上的例子:
- function DiagModule(stdlib, foreign, heap) {
- "use asm";
- // Variable Declarations
- var sqrt = stdlib.Math.sqrt;
- // Function Declarations
- function square(x) {
- x = +x;
- return +(x*x);
- }
- function diag(x, y) {
- x = +x;
- y = +y;
- return +sqrt(square(x) + square(y));
- }
- return { diag: diag };
- }
看看这个模块,它完全能让人理解!阅读这些代码,我们能更好的理解Asm.js模块的结构。一个模块包含在一个函数中,它以顶部的"use asm";指令开始。它提示解释器这个函数里所有的东西可以被当成Asm.js处理,并可以直接被编译成汇编代码。
注意在函数顶部的三个参数:stdlib,foreigh和heap。stdlib对象包含了很多内建数学函数的引用。foreign提供了自定义用户功能(例如在WebGL中绘制图形)的访问。最后,heap给了你一个ArrayBuffer,它可以通过很多透镜(例如Int32Array和Float32Array)来观察。
该模块剩下的被分成了三部分:变量声明,函数声明,还有最后把函数导出暴露给用户的一个对象。
导出是尤其要去理解的一个重点,因为它既让所有模块中的代码被当成Asm.js处理,又使得代码可以被其他普通的Javascript代码使用。因此,在理论上你可以通过使用上面的DiagModule代码,写下如下代码:
- document.body.onclick = function() {
- function DiagModule(stdlib){"use asm"; ... return { ... };}
- var diag = DiagModule({ Math: Math }).diag;
- alert(diag(10, 100));
- };
这带来了一个Asm.js模块DiagModule,它被Javascript解释器特殊处理,但仍然能够被其他Javascript代码使用(我们仍能访问并使用它,比如一个单击事件处理程序)。
性能如何?
现在Asm.js唯一的实现就是nightly versions of Firefox(而且也只是针对特定的几个平台)。原来的数字告诉我们Ams.js的性能是非常非常不错的。对于复杂的应用(比如上面的游戏)性能仅仅比普通C++编译的慢两倍(可以和Java或者C#相媲美)。实际上,这已经比目前浏览器的运行时环境要快很多了,几乎是最新版的Firefox或者Chrome执行速度的4~10倍。
基于目前最好测试,可以看出Asm.js在性能上有很大的提升。考虑到现在仅仅是Asm.js的最初开发阶段,相信在不久的将来就会有更大的性能提升。
看到Asm.js和当前的Firefox和Chrome引擎的性能差距是很有意思的。一个4~10倍的性能差异是非常巨大的(就好像拿这些浏览器和IE6 做性能对比一样)。有趣的是虽然有这么大的性能差异,但是许多的Asm.js演示例子仍然是可以在Chrome和Firefox——这些代表着当前 Javascript先进技术的引擎——上使用的。这也就是说他们的性能明显不如一个运行着优化过的Asm.js代码的浏览器相提并论。
使用情况
需要说明的是现在几乎所有基于Asm.js的应用都是C/C++应用使用Emscripten编译的。可以肯定的说,在不久的将来,这类即将运行在 Asm.js的应用,将会从可以在浏览器中运行这一可移植性中获益,但是在支持javascript方面有一定复杂度的应用将不可行。
到目前为止,大部分的使用情况下,代码性能是至关重要的:比如运行游戏,图像,处理语言翻译和库。从一个关于Emscripten项目列表的概览可以看到许多即将被广大开发者使用的技术。
- 许多游戏引擎已经被支持。一个好的演示也许就是BananaBread FPS游戏(源代码),它可以直接在浏览器中运行,支持多玩家和机器人。
- 支持javascript的LaTeX,即textlive.js,使用Emscripten,允许你完全在你的浏览器中编辑PDF文档。
- 支持javascript的SQLite,能够在Node.js中运行。
- NaCL:一个网络加密解密的库。
Asm.js支持
就像之前提到的那样,现在只有nightly版本的Firefox支持Asm.js的优化。
但是,要注意Asm.js格式的Javascript代码仍然是Javascript,虽然其存在一些限制。这样,其他的浏览器即使不支持Asm.js仍可以将其作为普通的Javascript代码运行。
有关代码性能重要而令人不解的一点是:如果浏览器不支持typed array或者不能对Asm.js代码进行特殊的编译,Asm.js的性能会变的很差。当然,这并不止针对Asm.js,如果没有这些特性,浏览器的性能在其他方面也会收到影响。
Asm.js与Web开发
你可能已经看出来了,上面的Asm.js代码不是手工输入的的。Asm.js需要一些特殊的工具来编写,而且其开发和编写普通的 Javascript代码有很大区别。目前一般的Asm.js应用都是从C/C++编译到Javascript的,很显然它们都不会与DOM进行任何交 互,而是直接与WebGL打交道。
为了能让一般的开发者使用,需要一些更能让人接受的中间语言。目前LLJS已经逐渐实现向Asm.js编译了。需要注意的是,LLJS这样的语言同样与常规的Javascript有很大区别,会让许多Javascript开发者感到困扰。即使是用LLJS这样更加友好的语言,编写Asm.js必须要对复杂的代码进行优化,这恐怕只有资深开发者能够胜任了。
就算有了LLJS或者别的语言来帮助我们编写Asm.js,我们也没有同样性能优异的DOM可以使用。理想的环境应该是将LLJS与DOM一起编译产生单一的可执行二进制文件。我还想不出这样做性能会有多好,但是我想这么做!
与David Herman的问答
我写信给David Herman(Mozilla Research的高级研究员),向他询问了一些问题。他们是如何将Asm.js的各部分结合在一起的?他们又希望用户从中得到什么呢?他亲切地深入回答了这些问题,有些回复很有趣。我希望你们也同样能从中获得启发。
Asm.js的目标是什么?你们这个计划的目标人群是哪些?
我们的目标是让开放网络成为一个虚拟主机,使之变为其他语言和平台的编译目标平台。在最初的版本中,我们集中于编译较底层的语言:C和C++。我们的长期目标是为更高层的语言提供支持,例如结构对象和垃圾回收等特性。我们最终会让其支持诸如JVM和.NET之类的应用。 既然asm.js的确扩展了web的基础,潜在的用户群很广。其中一批用户就是那些想得到尽可能多的运算能力的游戏开发者。除此之外,有开创性的 web开发者总会用一切工具来完成目标,谁能料到他们的办法呢?所以我真心希望asm.js成为让我想不到的创新应用得以实现的技术。 |
创建一个利于用户访问版本的Asm.js是否合理呢,比如一个更新版本的LLJS?或者是扩展目前项目的范围,而不仅仅是一个编译器的目标语言?
绝对可能。事实上,我的同事James Long最近声明,他已经从LLJS上开辟了一个初级的分支,用来编译成为asm.js。在Mozilla 研究所的团队也试图和James的工作相配合,从而能够正式的让LLJS支持asm.js。 我的看法是,一般来说你想手工书写asm.js代码的场景非常的少,和任何汇编语言一样。更多的是,你想用具有丰富表达力的语言,而最终把它们高效编译为 asm.js。当然,当一种语言达到极致的表达力,例如javascript,你就会难以预测其性能。(我的朋友 Slava Egorov写一篇相当好的文章来描述用高级语言写高性能代码的挑战。LLJS的目标是作为一种中间地带,就像一种相对于asm.js汇编语言的C语言, 这样就比写原始的asm.js语言更加容易,而比常规的js有更可预知的性能。但是不象C,它仍然和常规的JS有着良好的互通。这样,你可以用JS来写你 的app的大部分,而对于那些高度消耗性能的地方,则可以专注用LLJS来完成。 |
有一个讨论是关于,在目前支持Asm.js的浏览器和不支持的浏览器之间,会出现一种因此更新而形成的性能的分界,类似与 2008/2009年中所发生的JavaScript性能竞争。虽然技术上来看,Asm.js的代码在现实中,可以运行于任意两者之上,而性能的不同,对 于大多数的场合而言将会有有明显的不同(译者注:原词是too crippling,意思大致是严重的伤害)。那么对于这种分界,以及高度限制Javascript,为什么你选择了Javascript作为编译的目标?为什么不是另一种取代javascript的语言或者是一种插件?(译者注:作者似乎是想说明,Javascript的限制对于所有浏览器都会有影响,那么对于不支持Asm.js的浏览器会有所伤害,所以为什么不选择别的语言或者是插件,这样就会只影响自己的产品)
首先,我不认为这种分界如你所定义的那么严重:我们做过一些出色的演示代码的编译工作,这些代码在目前的浏览器上工作的很好,而且可以得益于asm.js这样的性能“杀手”。 这 一点是当然的事实,你可以创建一个应用程序,依赖于asm.js所曾益的性能,而且是可用的。与此同时,就如同任何新的web平台的能力,应用程序可以决 定是否降低一些性能,减轻那些与计算密切相关的行为。对于一个应用在降低性能的时候工作,和一个应用完全不能工作是有些不同的。 更广泛的来看,记得在00年之后开始的浏览器性能的竞争对于今天的web有着显著的好处,并且应用伴随着浏览器得到了改善。我相信同样的事情会而且将发生在asmjs上。 |
拿Asm.js和Google的Native Client做比较,你觉得如何?他们似乎都有这类似的目标,同时Asmjs有着可以运行在任何支持javascript的地方的优势。有过对于两者之间的任何性能比较么?
嗯,Native Client有點不同,由於它其中配備了平臺相關的匯編代碼;我不認為Google會作為一種web內容技術而支持它(相對於是它用於Chrome Web Store 內容或者 Chrome 擴展而言),或者說,最近沒有。 便攜的Native Client (PNaCI)有著類似的目標,使用平臺無關的LLVM的bitcode來代替原始的匯編代碼。如你所說,asmjs的第一個優勢就是其和現有的瀏覽器兼 容。我們也不必去創建一個系統的接口和重復全部的Web API中所有的接口層,好像Pepper API那樣,由於asmjs訪問目前存在的API是通過直接使用Javascript的。最終,這將對於實現更加輕松有利:Luke Wagner在一個多月中,就把我們第一次實現的OdinMoney的實現版本移植到了Firefox,主要是靠他一個人的工作。因為asmjs沒有一大堆的系統調用和API,而且因為它是在Javascript語法的基礎上建立起來的,你可以重復使用現存的Javascript的引擎和web運行環境提供的全部機制。 我們本可以做一些和PNaCI的性能比較,不過這會有一定的工作量,而且我們更多的是專註於縮小和原始的native代码性能的距離。我們計劃建立一些自動化的性能測試,這樣就可以用圖表來描述相對於native C/C++編譯器,我們目前的進展。 |
Emscripten是另一个Mozilla项目,也是Asm.js的主要兼容代码的贡献者。有多少Asm.js的开发被Emscripten的项目需求所支配?Emscripten又从引擎的提升上获得了什么益处?
我们使用Emscripten作为Asm.js的第一个测试用例,通过这种途径来保证它能够正确适应实际本地应用的需求。当然Emscripten受益也 使他们想要支持的所有拥有本地应用的人受益,比如Epic Games,我们仅在几天之内就与之组建了开发团队,通过使用Emscripten和Asm.js来支持web版的Unreal Engine 3的开发 |
但是Asm.js能够使那些专注于底层javascript子集的任何人获益。举个例子,我们提到过的开发与Emscripten有相似功能的 Mandreel编译器的folks,我们相信他们可以从Asm.js中受益,就像不久前启动的Emscripten项目一样。 |
Alon Zakai正在编译我们的基准测试程序,这个程序只比本地代码要慢大约2倍,以前我们看到的数字是5倍到10倍或者20倍。这只是我们最初的 OdinMonkey版本,这个版本的asm.js支持Mozilla的SpiderMonkey javascript引擎。在接下来的几个月中,我希望看到更多的提升。 |
Asm.js的功能还会变动么?随着越来越多编译器开发者的加入,你赞不赞同加入更多的附加功能(比如更高级的数据结构)?
当然。Luke Wagner在Mozilla wiki上写了asm.js与 OdinMonkey路线图,里面讨论了我们未来的计划——我必须指出这里面没有什么已经确定了的但的确说出了我们正在努力的方向。我很愿意加入对ES6结构对象的支持。这将会提供垃圾回收机制与良好的类结构,帮助像JSIL这类编译器将C#和Java编译为Javascript。我们也希望加入ES7数值类型,这将提供32位浮点数和64位整数支持,也许还会为提供SIMD支持加入定长向量 |
可以做出JavaScript-to-Asm.js转译器么,它会被做出来么?
可以做出来,但会做么?不一定。想想盗梦空间中你每次进行梦中梦的情景,时间将会变得多慢?一样的道理,你要是想在JS中运行一个JS引擎那一定会非常慢的。我们不严格地算一算,如果asm.js比原生代码慢一倍,那在asm.js中运行一个JS引擎,这引擎将会比它正常的速度慢一倍。 当然,你总是可以在一个JS引擎中运行另一JS引擎,谁知道到底会怎样呢?现实中的性能从来就不像理论计算那样明确。我欢迎一些积极的hacker去尝试它。事实上,斯坦福的学生Alex Tatiyants已经用Emscripten将Mozilla的SpiderMonkey引擎编译为JS了——你所需做的只是设置Emscripten编译器参数让它产生asm.js代码,时间比我充裕的家伙可以尝试一下…… |
现在Asm.js还不能进行与有关DOM和浏览器的操作。创建一个Emscripten到Ams.js版本的DOM(就像DOM.js)如何?
|
编写Asm.js代码与编写一般的Javascript代码相比确实十分困难,你有什么工具可以提供给开发者和编译器作者呢?
|
我觉得你们很乐于和其他浏览器厂商一起工作,你们的合作和讨论进展如何?
没错。我们之间有过一些非正式的讨论,他们一直对我们给予鼓励,我相信我们能做的更好。我很乐观,我们可以与很多厂商一起发展asm.js,那时 它可以让我们轻松地在不改变架构的情况下开发应用。就像我说的,事实上Luke只用了几个月就开发出了OdinMonkey,这很令人激动。我很高兴收到V8引擎下asm.js的bug报告。 更重要的是,我希望开发者可以检查asm.js的源码,看看我们是怎么想的,并反馈给我们和其他浏览器厂商。 |