这是一个可深可浅的话题,会先简单介绍一下什么是asm.js,看看它长什么样子,再来聊聊asm.js为什么能带来高性能,会有一些简单的对比,然后再从工程的角度讲讲兼容性,如何打包使用等等。水平有限,肯定讲不透,就当是抛装引玉吧。
还记得前段时间的“围棋杀手”AlphaGo吧,设想一下,如果两台AlphaGo相遇,会擦出些什么火花?同样的算法,在同样的机器上跑,会是什么样的结果?两只“阿尔法狗”我是抓不到了,倒是可以看看微软写的两个AI棋手对战DEMO:Asm.js Chess Battle。
游戏说明:
On this page, we match two chess engines against one another. They are identical in nearly every detail, including source code! The only difference is that one has a flag set to be interpreted as asm.js, a specialized and strict JavaScript subset that allows JavaScript engines to employ specialized compilation to dramatically speed up execution.
Each turn is limited to 200ms – because the asm.js-optimized engine has a significant performance advantage, it can evaluate more moves per turn and has a substantially higher likelihood of victory. You can adjust the turn length and other variables in the demo to see how they affect the outcome of the game.在这个页面中,我们设置两个象棋引擎进行对战。 这两个引擎在各个细节上几乎一样,包括源代码! 唯一的区别是,其中一个引擎设置了标志,让浏览器按照asm.js规范(一个特定、严格、允许JavaScript引擎使用专门的编译来显着加快执行的JavaScript子集)来解释。
每回合“思考”时间为0.2s - 因为源码被浏览器解析为asm.js的象棋引擎具有显着的性能优势,所以它在每回合可以进行更多评估,因此具有更高的获胜可能性。
正所谓天下武功,唯快不破。运行得更快,在每一回合评估的可能越多,越容易取胜。
值得注意的是:仅在对asm.js规范做了优化的浏览器上运行才能看到上面的结果,如果你是Safari,那么抱歉,遵循asm.js规范的代码说不定会更慢。关于兼容性下面马上会提到。
由上面那个象棋对战的演示可以看到,即使在web上,我们仍有许多场景(例如web游戏等)对运行性能有着极高的要求。对于这种极高的要求,这里介绍一种解法:asm.js
asm.js不是一门新的语言,而是JavaScript的一个子集。由Mozilla于2013年提出,主要为了提升JS引擎执行代码的速度。通俗来说,同样是js代码,符合asm.js规范的代码对JS引擎更加友好,JS引擎在解释执行这些代码更加省心(例如不用进行变量类型推断了),也就更加快。
an extraordinarily optimizable, low-level subset of JavaScript
一个极度可优化的,面向底层的JavaScript子集。
如果上面的描述你还看得一头雾水的话,说不定你在下面官方的常见问题中能找到一些答案。
如果你想深入了解asm.js的规范或者说草案标准,推荐看官方草案。
下面我们来看一下同一个算法(参考网上),三种不同的“语言”(其实只有C++和js)实现。
① C++
// For x = 1 to infinity:
// if x not divisible by any member of an initially empty list of primes,
// add x to the list until we have 25,000
//
// x从1递增到无穷,如果x不能整除list里的所有数,
// 那么将x添加到list,直到list中有25,000个数
#include
class Primes {
public:
int getPrimeCount() const { return prime_count; }
int getPrime(int i) const { return primes[i]; }
void addPrime(int i) { primes[prime_count++] = i; }
bool isDivisibe(int i, int by) { return (i % by) == 0; }
bool isPrimeDivisible(int candidate) {
for (int i = 1; i < prime_count; ++i) {
if (isDivisibe(candidate, primes[i])) return true;
}
return false;
}
private:
volatile int prime_count;
volatile int primes[25000];
};
int main() {
Primes p;
int c = 1;
while (p.getPrimeCount() < 25000) {
if (!p.isPrimeDivisible(c)) {
p.addPrime(c);
}
c++;
}
printf("%d\n", p.getPrime(p.getPrimeCount()-1));
}
② js
// For x = 1 to infinity:
// if x not divisible by any member of an initially empty list of primes,
// add x to the list until we have 25,000
//
// x从1递增到无穷,如果x不能整除list里的所有数,
// 那么将x添加到list,直到list中有25,000个数
// 开始打点
// console.log("start");
var startTime = new Date();
function Primes() {
this.prime_count = 0;
this.primes = new Array(25000);
this.getPrimeCount = function() { return this.prime_count; }
this.getPrime = function(i) { return this.primes[i]; }
this.addPrime = function(i) {
this.primes[this.prime_count++] = i;
}
this.isPrimeDivisible = function(candidate) {
for (var i = 1; i <= this.prime_count; ++i) {
if ((candidate % this.primes[i]) == 0) return true;
}
return false;
}
};
function main() {
p = new Primes();
var c = 1;
while (p.getPrimeCount() < 25000) {
if (!p.isPrimeDivisible(c)) {
p.addPrime(c);
}
c++;
}
console.log(p.getPrime(p.getPrimeCount()-1));
}
main();
// 结束打点
// console.log("end");
console.log("raw_js.js: ",(new Date() - startTime)/1000,"s");
③ asm.js规范(对,你没看错,下面就是js代码)
function _main() {
var $0 = 0, $1 = 0, $10 = 0, $11 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $c = 0, $p = 0, $vararg_buffer = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 100016|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$vararg_buffer = sp;
$p = sp + 8|0;
$0 = 0;
$c = 1;
while(1) {
$1 = (__ZNK6Primes13getPrimeCountEv($p)|0);
$2 = ($1|0)<(25000);
if (!($2)) {
break;
}
$3 = $c;
$4 = (__ZN6Primes16isPrimeDivisibleEi($p,$3)|0);
if (!($4)) {
$5 = $c;
__ZN6Primes8addPrimeEi($p,$5);
}
$6 = $c;
$7 = (($6) + 1)|0;
$c = $7;
}
$8 = (__ZNK6Primes13getPrimeCountEv($p)|0);
$9 = (($8) - 1)|0;
$10 = (__ZNK6Primes8getPrimeEi($p,$9)|0);
HEAP32[$vararg_buffer>>2] = $10;
// 结束打点
// console.log("end");
console.log("asm.js: ",(new Date() - startTime)/1000, "s");
(_printf(672,$vararg_buffer)|0);
$11 = $0;
STACKTOP = sp;return ($11|0);
}
function __ZNK6Primes13getPrimeCountEv($this) {
$this = $this|0;
var $0 = 0, $1 = 0, $2 = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = $this;
$1 = $0;
$2 = HEAP32[$1>>2]|0;
STACKTOP = sp;return ($2|0);
}
function __ZN6Primes16isPrimeDivisibleEi($this,$candidate) {
$this = $this|0;
$candidate = $candidate|0;
var $$expand_i1_val = 0, $$expand_i1_val2 = 0, $$pre_trunc = 0, $0 = 0, $1 = 0, $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, $8 = 0, $9 = 0, $i = 0;
var label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = sp + 12|0;
$1 = $this;
$2 = $candidate;
$3 = $1;
$i = 1;
while(1) {
$4 = $i;
$5 = HEAP32[$3>>2]|0;
$6 = ($4|0)<($5|0);
if (!($6)) {
label = 6;
break;
}
$7 = $2;
$8 = $i;
$9 = ((($3)) + 4|0);
$10 = (($9) + ($8<<2)|0);
$11 = HEAP32[$10>>2]|0;
$12 = (__ZN6Primes10isDivisibeEii($3,$7,$11)|0);
if ($12) {
label = 4;
break;
}
$13 = $i;
$14 = (($13) + 1)|0;
$i = $14;
}
if ((label|0) == 4) {
$$expand_i1_val = 1;
HEAP8[$0>>0] = $$expand_i1_val;
$$pre_trunc = HEAP8[$0>>0]|0;
$15 = $$pre_trunc&1;
STACKTOP = sp;return ($15|0);
}
else if ((label|0) == 6) {
$$expand_i1_val2 = 0;
HEAP8[$0>>0] = $$expand_i1_val2;
$$pre_trunc = HEAP8[$0>>0]|0;
$15 = $$pre_trunc&1;
STACKTOP = sp;return ($15|0);
}
return (0)|0;
}
function __ZN6Primes8addPrimeEi($this,$i) {
$this = $this|0;
$i = $i|0;
var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, $7 = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = $this;
$1 = $i;
$2 = $0;
$3 = $1;
$4 = HEAP32[$2>>2]|0;
$5 = (($4) + 1)|0;
HEAP32[$2>>2] = $5;
$6 = ((($2)) + 4|0);
$7 = (($6) + ($4<<2)|0);
HEAP32[$7>>2] = $3;
STACKTOP = sp;return;
}
function __ZNK6Primes8getPrimeEi($this,$i) {
$this = $this|0;
$i = $i|0;
var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = $this;
$1 = $i;
$2 = $0;
$3 = $1;
$4 = ((($2)) + 4|0);
$5 = (($4) + ($3<<2)|0);
$6 = HEAP32[$5>>2]|0;
STACKTOP = sp;return ($6|0);
}
function __ZN6Primes10isDivisibeEii($this,$i,$by) {
$this = $this|0;
$i = $i|0;
$by = $by|0;
var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $6 = 0, label = 0, sp = 0;
sp = STACKTOP;
STACKTOP = STACKTOP + 16|0; if ((STACKTOP|0) >= (STACK_MAX|0)) abort();
$0 = $this;
$1 = $i;
$2 = $by;
$3 = $1;
$4 = $2;
$5 = (($3|0) % ($4|0))&-1;
$6 = ($5|0)==(0);
STACKTOP = sp;return ($6|0);
}
说实话,一开始看到上面asm.js
的代码,也是懵的,这和以前上课学汇编好像呀,然后各种位运算,各种汇编运算,高位低位全部涌入脑海。
先告诉大家一个好消息压压惊,asm.js虽然是js的一个子集,但asm.js设计是面向js执行引擎的,而不是面向普通开发者的,也就是说,这货不是给我们手写,这货不是给我们手写,这货不是给我们手写……看看下面吧。
既然手写是不太现实的,那代码是怎么生成的?又怎么实际用在项目里呢?下面给大家推荐一个生成asm.js代码的工具Emscripten。
Emscripten的作者之一kripken也是asm.js规范的草案起稿者之一。
Emscripten是一个基于LLVM(Low Level Virtual Machine,编译器框架系统)的项目,它能将C和C++编译成浏览器高度可优化的asm.js代码。这使得我们能够在web环境中以接近原生的速度运行C和C++,且不需要额外的浏览器插件。
简单来说,Emscripten
能将C和C++代码编译成asm.js代码,得益于asm.js本身的可优化程度比较高及浏览器相应的优化,使得编译出来的asm.js代码在web中能以接近原生的速度运行。至于为什么是C和C++,为什么不直接对手写的JavaScript代码进行优化?这个问题我们后面再探索,下面先简单过一遍Emscripten
的使用。
教程来自官方:
相信大家看完上面三个文档基本已经了解Emscripten
的使用,至于Emscripten
背后的原理,你可以理解为它就是一个编译器(感兴趣的可以了解一下,以前作业写过Pascal to C++的简单编译器,有些没弄懂的地方,但是相当有趣),将C、C++代码编译经过编译过程(分词,语法分析,语义分析,中间代码生成,代码优化,目标代码生成)编译成符合asm.js规范的JavaScript代码,并且生成的JavaScript能直接用于node和web环境。
基于上面的算法,我们来对比一下上面三种写法的执行时间,再来聊聊为什么。
首先我将raw_c.cpp文件按照Emscripten
的不同编译选项进行编译,编译命令如下:
.././emcc raw_c.cpp -o asm.js
.././emcc -O1 raw_c.cpp -o asm_O1.js
.././emcc -O2 raw_c.cpp -o asm_O2.js
.././emcc -O3 raw_c.cpp -o asm_O3.js
.././emcc -Oz raw_c.cpp -o asm_Oz.js
① node环境:
通过time命令得到代码执行的时间,结果(仅做了3,4次重复实验,结果不算绝对严谨,但能说明问题)如下。
② 浏览器环境(Chrome vs Safari):
下面只对比了raw.js
和asm.js
(不带优化选项的默认结果)。
值得注意的是,我们在Safari中发现,经过优化的代码反而比原生的js代码还慢,这里就涉及到“兼容性”问题了,而这个兼容性特指运行速度的兼容性,因为asm.js规范只是JavaScript的一个子集,所以asm.js能在任何版本的浏览器中运行,但不是在任何浏览器上都跑得这么快。
为了让大家更好地理解兼容性这个问题,我们先探讨一下,为什么遵循asm.js规范的代码要比手写的js代码运行快?
这大概是本文最核心的部分,也是没有信心说透的地方,暂且当做阶段性的学习总结。
实际上,由于JavaScript运行环境特性不一(除了node外还有各式各样的浏览器等),某一特性的表现与两方面有关,一是规范,二是实现。
可以这样理解,制定规范的“大哥们”说,
“来来来,兄弟们,今天我们来商量一件事,是这样的,昨晚我的Windows电脑坏了,手上的Mac又打不了《英雄联盟》 ,我想要是能在Web里打《英雄联盟》那多好,这样就不管什么Windows呀,Mac呀,Ubuntu…打开浏览器马上开撸,爽不爽?然后我昨晚就试着在web上写起了游戏来,结果一运行,我的天,这帧率怎么补刀,超神也别想了。于是苦闷得我整晚睡不着开始思考人生,首要问题还是,js在web上怎么运行这么慢呀?”
果然大哥就是大哥,熬了一晚之后的今天,大哥豁然开朗,
“我一想,王侯将相宁有种乎,凭啥C++写起游戏来这么快,我大JavaScript就玩不溜?于是ASM.js规范诞生了。兄弟们呀,你们按照这个规范做,保准跟着大哥在浏览器里面打撸把把超神呀!”
小弟们做着超神美梦纷纷点头支持,但人多想法也多嘛,有些小弟想,
“既然大哥都想好了,就按照asm.js这个规范做吧。”。
而有一些小弟比较有想法,
“其实大哥才不在意asm.js呢,大哥只是想在浏览器里面打撸而已嘛,我参考大哥的思路来提高JavaScript的运行速度就好啦。”
总之想法很多,反正经过努力,大家都愉快地在各家的浏览器打起了《英雄联盟》,而当初说的asm.js规范更像是一些建议,而不一定是最后的实现。“管他的,反正超神就好了……”
哈哈,大家看完理解了就行,里面说到的大哥小弟只是开个玩笑,没有半点调侃的意思,实际上规范的制定者和规范的实现者都是推动web前进的中流砥柱,我十分敬佩,而且制定者和实现者往往都是那么几个大神……
正经版概述:几个大神提出了asm.js规范,遵循该规范的代码性能更好,并且更容易被浏览器优化,从而性能更上一层楼。至于浏览器怎么优化,那就是各家的事情了。下面将从asm.js规范本身及主流浏览器的实现分别讲讲,为什么快?
从asm.js规范入手,快主要得益于以下几方面:
补充:
AOT编译:在运行前预编译(对比JIT:在运行中编译)
1,FireFox与Microsoft Edge
遵循asm.js规范,能进行AOT处理。
2,Chrome
不支持AOT,但是对asm.js规范进行了优化。参考Chrome官方博客.
3,Safari
尚未对asm.js进行优化,根据官方消息:
There is no “use asm” mode in JavaScriptCore. Instead WebKit integrates ASM.js optimizations directly in the optimizer. As a result, it is possible to mix ASM-style typing with regular code and still get great performance and power efficiency.
JavaScriptCore(Safari使用的JavaScript引擎)中不支持“use asm”模式。然而,Webkit直接将asm.js的优化集成到优化器中,将来,有可能将asm.js代码与常规代码结合起来,以达到更好的性能同时更加节约能耗。
上面也说了,因为asm.js是JavaScript的一个子集,也就是asm.js能正确运行在支持JavaScript的浏览器上,所以这里探讨的兼容性问题是“所有浏览器能不能高速运行asm.js代码?”而不是“所有各个浏览器能不能正确运行asm.js代码?”
Safari全系不支持对asm.js的优化。由之前的测试结果得出asm.js规范的代码在Safari上运行甚至有可能比原生js代码慢许多,Safari的缺席无疑在一定程度上扑灭了开发者的热情,这不得不令人想起Google强推的Service Worker同样在Safari中还没得到实现。
然而作为开发者对于技术的未来我们应该始终保持乐观,始终相信技术会朝着好的方向发展。现在可以看到的是由苹果公司主导的webkit项目对asm.js的优化支持已经处于开发阶段了。
在工程上,除了兼容性之外,我们往往还会考虑引入asm.js除了带来高性能福利外,会不会有其他副作用。这里的副作用就是体积。
我们前面对将raw_c.cpp
按照不同的编译选项编译成不同的asm_0*.js
,包体积如下:
注意:
asm_Oz.js.mem
文件的由来:
默认情况下,Emscripten
将静态内存初始化代码打包到js文件里面,这会导致包体积过大。将这部分代码分离到以.mem结尾的二进制文件可以解决这个问题,asm_Oz.js
执行的时候,会异步加载asm_Oz.js.mem
,加载完成后再执行主函数。
可见,经过压缩优化后的asm_Oz.js
大约有180k的额外负担,可以在服务器启动gzip压缩将负担降至最少,这部分主要是起始的代码,后面包体积只会随业务代码体积增大而增大,和以前一致。
总的来说,包体积带来的负担在产品环境还是能接受的,而且,一旦你的项目都要启用asm.js这种优化技术了,相信100k对你来说根本不是瓶颈。
由于博客已经写得过长了,这里就只给出官方文档,然后大致过一遍我认为不错的开发配置。
C++与JavaScript的交互
最最简单的使用是将js执行时间较长的代码用C++的方法实现一遍,可以在C++中暴露成为一个个方法,或者定义成为一个类,里面包含了一系列方法。至于怎么在JavaScript中调用C++编译后的asm.js代码中暴露出来的方法或类方法,相信上面的文档已经说得足够清楚了。
注意,在写C++代码的时候,除了可以使用一些标准库之外,不能使用任何DOM,BOM之类的API。
不得不说,asm.js是给我无限想象的一个规范。
JavaScript之所以深得大家喜爱,是因为它能运行在所有浏览器上,而web形态是目前连接性,传递性最强的形态。而JavaScript能做什么也一定程度上决定了我们在web上能做什么。那么如果现在我们利用Emscripten
能将其他语言编译成可高性能执行的JavaScript,会发生什么?例如,将C++编译器编译成asm运行在浏览器上,那么任何的C++程序是不是就能实时地在浏览器中编译运行?
任何东西都将有可能运行在web。
题外话:JavaScript与我都是95年来到这个世界的,21年来,JavaScript与我都在跌跌撞撞中成长起来了,然而作为一个青少年,未来依然是未知的,但我仍然保持一贯的乐观。
else
JavaScript的诞生和死亡