WebAssembly与程序编译

转自:https://fed.renren.com/2017/05/21/webassembly/

Webassembly(WASM)和CSS的Grid布局一样都是一个新东西,Chrome从57开始支持。在讲wasm之前我们先看代码是怎么编译的成机器码,因为计算机只认识机器码。

1. 机器码

计算机只能运行机器码,机器码是一串二进制的数字,如下面的可执行文件a.out:

上面显示成16进制,是为了节省空间。

例如我用C写一个函数,如下:

int main(){

    int a = 5;

    int b = 6;

    int c = a + b;

    return 0;

}

int main(){

    int a = 5;

    int b = 6;

    int c = a + b;

    return 0;

}

然后把它编译成一个可执行文件,就变成了上面的a.out。a.out是一条条的指令组成的,如下图所示,研究一下为了做一个加法是怎么进行的:

第一个字节表示它是哪条指令,每条指令的长度可能不一样。上面总共有四条指令,第一条指令的意思是把0x5即5这个数放到内存内置为[rbp –

0x8]的位置,第二条指令的意思是把6放到内存地址为[rbp –

0xc]的位置,为什么内存的位置是这样呢,因为我们定义了两个局部变量a和b,局部变量是放在栈里面的,而new出来的是放在内存堆里面的。上面main函数的内存栈空间如下所示:

rbp是一个base

pointer,即当前栈的基地址,这里应该为main函数入口地地址,然后又定义了两个局部变量,它们依次入栈,栈由下往上增长,向内存的低位增长,在我的这个Linux操作系统上是这样的。最后return返回的时候这个栈就会一直pop到入口地址位置,回到调它的那个函数的地址,这样你就知道函数栈调用是怎么回事了。

一个栈最大的空间为多少呢?可以执行ulimit -s或者ulimit -a命令,它会打印出当前操作系统的内存栈最大值:

> ulimit -a

stack size              (kbytes, -s) 8192

这里为8Mb,相对于一些OS默认的64Kb,已经是一个比较大的值了。一旦超出这个值,就会发生栈溢出stack overflow.

理解了第一条指令和第二条指令的意思后就不难理解第三条和第四条了。第三条是把内存地址为[rbp – 8]放到ecx寄存器里面,第四条做一个加法,把[rbp – 12]加到ecx寄存器。就样就完成了c = a + b的加法。

更多汇编和机器码的运算读者有兴趣可以自行去查资料继续扩展,这里我提了一下,帮助读者理解这种比较较陌生的机器码是怎么回事,也是为了下面讲解WASM.

2. 编译和解释

我们知道编程语言分为两种,一种是编译型的如C/C++,另一种是解释型如Java/Python/JS等。

在编译型语言里面,代码需经过以下步骤转成机器码:

先把代码文本进行词法分析、语法分析、语义分析,转成汇编语言,其实解释型语言也是需要经过这些步骤。通过词法分析识别单词,例如知道了var是一个关键词,people这个单词是自定义的变量名字;语法分析把单词组成了短句,例如知道了定义了一个变量,写了一个赋值表达式,还有一个for循环;而语义分析是看逻辑合不合法,例如如果赋值给了this常量将会报错。

再把汇编再翻译成机器码,汇编和机器码是两个比较接近的语言,只是汇编不需要去记住哪个数字代表哪个指令。

编译型语言需要在运行之前生成机器码,所以它的执行速度比较快,比解释型的要快若干倍,缺点是由于它生成的机器码是依赖于那个平台的,所以可执行的二进制文件无法在另一个平台运行,需要再重新编译。

相反,解释型为了达到一次书写,处处运行(write once, run evrywhere)的目的,它不能先编译好,只能在运行的时候,根据不同的平台再一行行解释成机器码,导致运行速度要明显低于编译型语言。

如果你看Chrome源码的话,你会发现V8的解释器是一个很复杂的工程,有200多个文件:

然后来讲WebAssembly了。

3. WebAssembly介绍

WASM的意义在于它不需要JS解释器,可直接转成汇编代码(assembly code),所以运行速度明显提升,速度比较如下:

通过一些实验的数据,JS大概比C++慢了7倍,ASM.js官网认为它们的代码运行效率是用clang编译的代码的1/2,所以就得到了上面比较粗糙的对比。

Mozilla公司最开始开发asm.js,后来受到Chrome等浏览器公司的支持,慢慢发展成WASM,W3C还有一个专门的社区,叫WebAssembly Community Group。


WASM是JS的一个子集,它必须是强类型的,并且只支持整数、浮点数、函数调用、数组、算术计算,如下使用asm规范写的代码做两数的加法:

function () {

    "use asm";

    function add(x, y) {

        x = x | 0;

        y = y | 0;

        return x | 0 + y | 0;

    }

    return {add: add};

}

function () {

    "use asm";

    function add(x, y) {

        x = x | 0;

        y = y | 0;

        return x | 0 + y | 0;

    }

    return {add: add};

}

正如asm.js官网提到的:

An extremely restricted subset of JavaScript that provides onlystrictly-typed integers, floats, arithmetic, function calls, and heap accesses

WASM的兼容性,如caniuse所示:

最新的主流浏览器基本上已经支持。

 4. WASM Demo

(1)准备

Mac电脑需要安装以下工具:

cmake make Clang/XCode

Windows需要安装:

cmake make VS2015 以上

然后再装一个

WebAssembly binaryen (asm2Wasm)

(2)开始

写一个add.asm.js,按照asm规范,如下图所示:

然后再运行刚刚装的工具asm2Wasm,就可以得到生成的wasm格式的文本,如下图所示

可以看到WASM比较接近汇编格式,可以比较方便地转成汇编。

如果不是在控制台输出,而是输出到一个文件,那么它是二进制的。运行以下命令:

> ../bin/asm2wasm add.asm.js -o add.wasm

打开生成的add.wasm,可以看到它是一个二进制的:

有了这个文件之后怎么在浏览器上面使用呢,如下代码所示,使用Promise,与WebAssembly相关的对象本身就是Promise对象:

fetch("add.wasm").then(response =>

    response.arrayBuffer())

.then(buffer =>

    WebAssembly.compile(buffer))

.then(module => {

    var imports = {env: {}};

    Object.assign(imports.env, {

        memoryBase: 0,

        tableBase: 0,

        memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),

        table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })

  })

  var instance =  new WebAssembly.Instance(module, imports)

  var add = instance.exports.add;

  console.log(add, add(5, 6));

})

fetch("add.wasm").then(response =>

    response.arrayBuffer())

.then(buffer =>

    WebAssembly.compile(buffer))

.then(module => {

    var imports = {env: {}};

    Object.assign(imports.env, {

        memoryBase: 0,

        tableBase: 0,

        memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),

        table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })

   })

   var instance =  new WebAssembly.Instance(module, imports)

   var add = instance.exports.add;

   console.log(add, add(5, 6));

})

先去加载add.wasm文件,接着把它编译成机器码,再new一个实例,然后就可以用exports的add函数了,如下控制台的输出:

可以看到add函数已经变成机器码了。

现在来写一个比较有用的函数,斐波那契函数,先写一个asm.js格式的,如下所示:

function fibonacci(fn, fn1, fn2, i, num) {

    num = num | 0;

    fn2 = fn2 | 0;

    fn = fn | 0;

    fn1 = fn1 | 0;

    i = i | 0;

    if(num < 0)  return 0;

    else if(num == 1) return 1;

    else if(num == 2) return 1;

    while(i <= num){

        fn = fn1;

        fn1 = fn2;

        fn2 = fn + fn1;

        i = i + 1;

    } 

    return fn2 | 0;

}

function fibonacci(fn, fn1, fn2, i, num) {

    num = num | 0;

    fn2 = fn2 | 0;

    fn = fn | 0;

    fn1 = fn1 | 0;

    i = i | 0;

    if(num < 0)  return 0;

    else if(num == 1) return 1;

    else if(num == 2) return 1;

    while(i <= num){

        fn = fn1;

        fn1 = fn2;

        fn2 = fn + fn1;

        i = i + 1;

    }  

    return fn2 | 0;

}

这里笔者最到一个问题,就是定义的局部变量无法使用,它的值始终是0,所以先用传参的方式。

然后再把刚刚那个加载编译的函数封装成一个函数,如下所示:

loadWebAssembly("fibonacci.wasm").then(instance => {

    var fibonacci = instance.exports.fibonacci;

    var i = 4, fn = 1, fn1 = 1, fn2 = 2;

    console.log(i, fn, fn1, fn2, "f(5) = " + fibonacci(5));

});

loadWebAssembly("fibonacci.wasm").then(instance => {

    var fibonacci = instance.exports.fibonacci;

    var i = 4, fn = 1, fn1 = 1, fn2 = 2;

    console.log(i, fn, fn1, fn2, "f(5) = " + fibonacci(5));

});

最后观察控制台的输出:

可以看到在f(47)的时候发生了溢出,在《JS与多线程》这一篇提到JS溢出了会自动转成浮点数,但是WASM就不会了,所以可以看到WASM/ASM其实和JS没有直接的关系,只是说你可以用JS写WASM,虽然官网的说法是ASM是JS的一个子集,但其实两者没有血肉关系,用JS写ASM你会发现非常地笨拙和不灵活,编译成WASM会有各种报错,提示信息非常简陋,总之很难写。但是不用沮丧,因为下面我们会提到还可以用C写。

然后我们可以做一个兼容,如果支持WASM就去加载wasm格式的,否则加载JS格式,如下所示:

5. JS和WASM的速度比较

(1)运行速度的比较

如下代码所示,计算1到46的斐波那契值,然后重复一百万次,分别比较wasm和JS的时间:

//wasm运行时间

loadWebAssembly("fib.wasm").then(instance => {

    var fibonacci = instance.exports._fibonacci;

    var num = 46;

    var count = 1000000;

    console.time("wasm fibonacci");

    for(var k = 0; k < count; k++){

        for(var j = 0; j < num; j++){

            var i = 4, fn = 1, fn1 = 1, fn2 = 2;

            fibonacci(fn, fn1, fn2, i, j);

        }

    }

    console.timeEnd("wasm fibonacci");

});

//js运行时间

loadWebAssembly("fibonacci.js", {}, "js").then(instance => {

    var fibonacci = instance.exports.fibonacci;

    var num = 46;

    var count = 1000000;

    console.time("js fibonacci");

    for(var k = 0; k < count; k++){

        for(var j = 0; j < num; j++){

            var i = 4, fn = 1, fn1 = 1, fn2 = 2;

            fibonacci(fn, fn1, fn2, i, j);

        }

    }

    console.timeEnd("js fibonacci");

});

//wasm运行时间

loadWebAssembly("fib.wasm").then(instance => {

    var fibonacci = instance.exports._fibonacci;

    var num = 46;

    var count = 1000000;

    console.time("wasm fibonacci");

    for(var k = 0; k < count; k++){

        for(var j = 0; j < num; j++){

            var i = 4, fn = 1, fn1 = 1, fn2 = 2;

            fibonacci(fn, fn1, fn2, i, j);

        }

    }

    console.timeEnd("wasm fibonacci");

});


//js运行时间

loadWebAssembly("fibonacci.js", {}, "js").then(instance => {

    var fibonacci = instance.exports.fibonacci;

    var num = 46;

    var count = 1000000;

    console.time("js fibonacci");

    for(var k = 0; k < count; k++){

        for(var j = 0; j < num; j++){

            var i = 4, fn = 1, fn1 = 1, fn2 = 2;

            fibonacci(fn, fn1, fn2, i, j);

        }

    }

    console.timeEnd("js fibonacci");

});

运行四次,比较如下:

可以看到,在这个例子里面WASM要比JS快了一倍。

然后再比较解析的时间

(2)解析时间比较

如下代码所示:

console.time("wasm big content parse");

loadWebAssembly("big.wasm").then(instance => {

    var fibonacci = instance.exports._fibonacci;

    console.timeEnd("wasm big content parse");

    console.time("js big content parse");

    loadJs();

});

function loadJs(){

  loadWebAssembly("big.js", {}, "js").then(instance => {

      var fibonacci = instance.exports.fibonacci;

      console.timeEnd("js big content parse");

  });

}

console.time("wasm big content parse");

loadWebAssembly("big.wasm").then(instance => {

    var fibonacci = instance.exports._fibonacci;

    console.timeEnd("wasm big content parse");

    console.time("js big content parse");

    loadJs();

});

function loadJs(){

   loadWebAssembly("big.js", {}, "js").then(instance => {

       var fibonacci = instance.exports.fibonacci;

       console.timeEnd("js big content parse");

   });

}

分别比较解析100、2000、20000行代码的时间,统计结果如下:

WASM的编译时间要高于JS,因为JS定义的函数只有被执行的时候才去解析,而WASM需要一口气把它们都解析了。

上面表格的时间是一个什么概念呢,可以比较一下常用库的解析时间,如下图所示:

(3)文件大小比较

20000行代码,wasm格式只有3.4k,而压缩后的js还有165K,如下图所示:

所以wasm文件小,它的加载时间就会少,可以一定程度上弥补解析上的时间缺陷,另外可以做一些懒惰解析的策略。

6. WASM的优缺点

WASM适合于那种对计算性能特别高的,如图形计算方面的,缺点是它的类型检验比较严格,写JS编译经常会报错,不方便debug。

WASM官网提供的一个WebGL + WebAssembly坦克游戏如下所示:

它的数据和函数都是用的wasm格式:

7. C/Rust写前端

WASM还支持用C/Rust写,需要安装一个emsdk。然后用C函数写一个fibonacci.c文件如下所示:

/* 不考虑溢出 */

int fibonacci(int num){

    if(num <= 0) return 0;

    if(num == 1 || num == 2) return 1;

    int fn = 1,

        fn1 = 1,

        fn2 = fn + fn1;

    for(int i = 4; i <= num; i++){

        fn = fn1;

        fn1 = fn2;

        fn2 = fn1 + fn;

    }

    return fn2;

}

/* 不考虑溢出 */

int fibonacci(int num){

    if(num <= 0) return 0;

    if(num == 1 || num == 2) return 1;

    int fn = 1,

        fn1 = 1,

        fn2 = fn + fn1;

    for(int i = 4; i <= num; i++){

        fn = fn1;

        fn1 = fn2;

        fn2 = fn1 + fn;

    }

    return fn2;

}

运行以下命令编译成一个wasm文件:

emcc fibonacci.c -Os -s WASM=1 -s SIDE_MODULE=1 -o fibonacci.wasm

这个wasm和上面的是一样的格式,然后再用同样的方式在浏览器加载使用。

用C写比用JS写更加地流畅,定义一个变量不用在后面写一个“| 0”,编译起来也非常顺畅,一次就过了,如果出错了,提示非常友好。这就可以把一些C库直接挪过来前端用。

8. WASM对写JS的提示

WASM为什么非得强类型的呢?因为它要转成汇编,汇编里面就得是强类型,这个对于JS解释器也是一样的,如果一个变量一下子是数字,一下子又变成字符串,那么解释器就得额外的工作,例如把原本的变量销毁再创建一个新的变量,同时代码可读性也会变差。所以提倡:

定义变量的时候告诉解释器变量的类型

不要随意改变变量的类型

 函数返回值类型是要确定的

这个我在《Effective前端8:JS书写优化》已经提到.


到此,介绍完毕,通过本文应该对程序的编译有一个直观的了解,特别是代码是怎么变成机器码的,还有WebAssembly和JS的关系又是怎么样的,Webassembly是如何提高运行速度,为什么要提倡强类型风格代码书写。对这些问题应该可以有一个理解。

另外一方面,web前端技术的发展真的是非常地活跃,在学这些新技术的同时,别忘了打好基本功。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,053评论 1 10
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,716评论 2 17
  • 一、JavaScript基础知识回顾 1.1 JavaScript 1.1.1 javascript是什么? Ja...
    福尔摩鸡阅读 1,226评论 0 7
  • 函数的声明和调用JavaScript是一种描述型脚本语言,由浏览器进行动态的解析与执行。函数的定义方式大体有以下两...
    Marco_Deng阅读 439评论 0 0
  • 如何控制alert中的换行?\n alert(“p\np”); 请编写一个JavaScript函数 parseQu...
    heyunqiang99阅读 1,083评论 0 6