asm.js:面向未来的开发

这是一个可深可浅的话题,会先简单介绍一下什么是asm.js,看看它长什么样子,再来聊聊asm.js为什么能带来高性能,会有一些简单的对比,然后再从工程的角度讲讲兼容性,如何打包使用等等。水平有限,肯定讲不透,就当是抛装引玉吧。

What’s 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:面向未来的开发_第1张图片

正所谓天下武功,唯快不破。运行得更快,在每一回合评估的可能越多,越容易取胜。

值得注意的是:仅在对asm.js规范做了优化的浏览器上运行才能看到上面的结果,如果你是Safari,那么抱歉,遵循asm.js规范的代码说不定会更慢。关于兼容性下面马上会提到。

主角登场

由上面那个象棋对战的演示可以看到,即使在web上,我们仍有许多场景(例如web游戏等)对运行性能有着极高的要求。对于这种极高的要求,这里介绍一种解法:asm.js

asm.js不是一门新的语言,而是JavaScript的一个子集。由Mozilla于2013年提出,主要为了提升JS引擎执行代码的速度。通俗来说,同样是js代码,符合asm.js规范的代码对JS引擎更加友好,JS引擎在解释执行这些代码更加省心(例如不用进行变量类型推断了),也就更加快。

asm.js:面向未来的开发_第2张图片

an extraordinarily optimizable, low-level subset of JavaScript
一个极度可优化的,面向底层的JavaScript子集。

如果上面的描述你还看得一头雾水的话,说不定你在下面官方的常见问题中能找到一些答案。

如果你想深入了解asm.js的规范或者说草案标准,推荐看官方草案。

asm.js、js、C++

下面我们来看一下同一个算法(参考网上),三种不同的“语言”(其实只有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代码

既然手写是不太现实的,那代码是怎么生成的?又怎么实际用在项目里呢?下面给大家推荐一个生成asm.js代码的工具Emscripten。

Emscripten的作者之一kripken也是asm.js规范的草案起稿者之一。

asm.js:面向未来的开发_第3张图片

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的使用。

教程来自官方:

  1. 下载和安装
  2. 起步
  3. 优化选项

相信大家看完上面三个文档基本已经了解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次重复实验,结果不算绝对严谨,但能说明问题)如下。

asm.js:面向未来的开发_第4张图片

性能对比

性能对比

asm.js:面向未来的开发_第5张图片

② 浏览器环境(Chrome vs Safari):

下面只对比了raw.jsasm.js(不带优化选项的默认结果)。

asm.js:面向未来的开发_第6张图片

值得注意的是,我们在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规范本身及主流浏览器的实现分别讲讲,为什么快?

规范or理论

从asm.js规范入手,快主要得益于以下几方面:

  • 数据类型确定 (使用位运算等限制数据类型)
  • “use asm”指示浏览器跳过数据类型等验证
  • 合法的asm.js代码能进行AOT提前编译处理(其中能实现复杂的性能优化)
  • 没有垃圾回收机制,内存的分配和回收时机都是手动确定的

补充:
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代码?

asm.js:面向未来的开发_第7张图片

Safari全系不支持对asm.js的优化。由之前的测试结果得出asm.js规范的代码在Safari上运行甚至有可能比原生js代码慢许多,Safari的缺席无疑在一定程度上扑灭了开发者的热情,这不得不令人想起Google强推的Service Worker同样在Safari中还没得到实现。

然而作为开发者对于技术的未来我们应该始终保持乐观,始终相信技术会朝着好的方向发展。现在可以看到的是由苹果公司主导的webkit项目对asm.js的优化支持已经处于开发阶段了

asm.js:面向未来的开发_第8张图片

打包

在工程上,除了兼容性之外,我们往往还会考虑引入asm.js除了带来高性能福利外,会不会有其他副作用。这里的副作用就是体积。
我们前面对将raw_c.cpp按照不同的编译选项编译成不同的asm_0*.js,包体积如下:

asm.js:面向未来的开发_第9张图片

注意:asm_Oz.js.mem文件的由来:
默认情况下,Emscripten将静态内存初始化代码打包到js文件里面,这会导致包体积过大。将这部分代码分离到以.mem结尾的二进制文件可以解决这个问题,asm_Oz.js执行的时候,会异步加载asm_Oz.js.mem,加载完成后再执行主函数。

可见,经过压缩优化后的asm_Oz.js大约有180k的额外负担,可以在服务器启动gzip压缩将负担降至最少,这部分主要是起始的代码,后面包体积只会随业务代码体积增大而增大,和以前一致。

总的来说,包体积带来的负担在产品环境还是能接受的,而且,一旦你的项目都要启用asm.js这种优化技术了,相信100k对你来说根本不是瓶颈。

C++与JS交互

由于博客已经写得过长了,这里就只给出官方文档,然后大致过一遍我认为不错的开发配置。

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的诞生和死亡

你可能感兴趣的:(前端)