一切可编译为WebAssembly的,终将被编译为WebAssembly。

V8执行原理

从整体流程来看,v8引擎执行js代码的关键步骤在于

  • AST语法树生成
  • 字节码生成
  • 解释器
  • JIT(优化编译器,监控模块)

对于整体流程的解释与分析这里不再赘述,详情可查看北渊大佬的博客v8 执行 js 的过程

JIT和AOT定义&对比

JIT和AOT是对当下两种最为流行的代码编译方式的定义

JIT

JIT(Just-In-Time):即时编译,又称动态编译

  • JIT的特点在于不对代码做完整的编译,而是随着程序的运行逐行编译

AOT

AOT(Ahead-Of-Time):预先编译,又称静态编译

  • AOT的特点在于在程序运行之前将所有代码进行编译,而后将完整的程序机器码交由操作系统执行

Differences

两者的区别

(ps:两者的区别就好像当我们看一篇外文文章时采用不同的翻译方式)

  • JIT的方式就像我们在阅读文章时采用划词翻译的方式,我们可以完全根据自己的进度去翻译文章

    • 启动速度快,不需要等待全文翻译
    • 优化,根据当前的上下文对翻译内容进行优化
  • AOT的方式更像google全文翻译,在阅读文章之前,google会将整篇文章转化为我们熟知的语言

    (妈妈再也不用担心我看不懂英文了)

    • 阅读速度快,不需要再收到外语的干扰
    • 减少思考时间,不需要去对上下文分析解析语意

常用语言分类

AOT:c/c++,Go

JIT:javascript,python

解释型语言和编译型语言的区分很多时候都是靠编译的种类来区分,一般而言,使用AOT方式编译的语言都被称为编译型语言

JavaScript的(性史)性能史

早期V8的传说

早期的V8引擎为了提高javascript的运行效率,曾一度放弃JavaScriptCore引擎(WebKit内核的js引擎)中的“字节码解释器”,直接在AST语法树的基础上进行编译,将JavaScript直接编译为机器码,以达到近乎二进制代码的运行速度,其执行过程核心组成大致如下:

  • Full-codegen编译器

    又称基线编译器,是v8引擎中最简单的编译器,它可以基于AST抽象语法树直接编译成机器码,因其在编译过程中对机器码不做任何优化,生成基准的未经优化的机器码,故也称为基准编译器。

  • Crankshaft优化编译器

    CrankShaft是V8引擎中最早使用的优化编译器,在传统意义上,编译过程应当会阻塞代码的运行,但Crankshaft的执行却不会,它是运行在另一个线程上的(注:这与JS本身单线程并不冲突,JS的单线程指的是JS执行过程中只有一个调用栈)

  • TurboFan优化编译器

    尽管在很多文章中,大家都更倾向于将TurboFan优化编译器归结于V8引擎5.9版本以后的特点,但从V8的官方blog来看,TurboFan早在2015 年 7 月 13 日(划重点,下文要用)V8引擎4.5版本之前就已经开始有部分支持了,它具有新的分层架构,改变了传统编译器随着时间推移而变得复杂的问题,使得该编译器能够随着时间的推移应对复杂需求。

从以上的核心来看,早期V8引擎实际上是没有解释器这一核心的,所有JS代码的执行全都是由编译器去进行处理,直接转换为操作系统可以运行的机器码,由于跳过了一个大步骤(字节码),所以其速度在当时远超其他JS引擎。

结束传说的bug

过于超前的实践必然有其问题,为什么在浏览器引擎发展史中只有V8将字节码省略,直接生机器码,为什么其他引擎没有做出这样的改变?这一切的一切都与机器码本身有关,而V8去除字节码的这一操作也在发布不久之后产生了问题。

正如我们所知道的,所谓高级语言,其实都是对机器码进行一层又一层的“抽象”,可以说正是因为抽象水平的提高,才有了当下各种高级语言的发展,这些高度抽象的语言让人们得以用少量的代码完成复杂的计算机操作,但是,无论多么抽象的语言,他们最终的目的都是转换为机器码以让操作系统去执行这些操作,作为动态类型语言的JavaScript其抽象程度远比其他高级语言要高,因为其不确定的类型系统,使得其转换出的机器码远超其他语言,省去字节码这一操作的问题也就随之明了:

  • 大量的机器码会占据浏览器大量的内存,从而减缓了执行速度

  • 对机器码进行缓存时,无法对所有机器码缓存

  • 根据V8的策略,往往是从最外层向内缓存,这就造成很多时候我们缓存的代码都只是最外层的生明,而不是内部的实现

  • V8将方法的实现留到第一次执行时才去编译,因而会造成重复解析的现象

  • 由于机器码的体积较大,在运行过程中有许多只用到一次的代码也会被变为机器码缓存,极大浪费资源

    ……

总的来说,去除字节码这一操作在当时还是产生了许多问题,这也就证明了字节码的重要性。

V8的重生

v8解释器 v8优化编译器

说到V8引擎,其实大家都很喜欢提到一个特殊的版本V5.9,这个版本中,V8引擎进行了重大变革,将原有的基线编译器(Full-codegen)和优化编译器(CrankShaft)抛弃,正式启用了Ignition解释器(左),TurboFan优化编译器(右)

这一举措将V8引擎再一次带回到字节码时代,通过使用比机器码体积小许多的字节码,解决了机器码占用大量缓存的现象,同时提高了代码的启动速度,这一方式自2017 年 4 月 27 日发布,也是至今仍然持续使用的执行管道,V8也借此机会优化了本身的代码结构。

WebAssembly发展史

起于性能

事实上,在V8引擎将JIT引入JS引擎开始,JS的性能已经达到了一个很高的水平,对于大部分日常业务来说,性能方面甚至是绰绰有余的,但这仅仅是针对日常业务来讲(类似于crud,按钮弹窗交互这种),如果以对性能要求更高一等的游戏来讲,V8引擎对于稍有量级的游戏仍然是心有余而力不足,这便使得V8开始向更高的性能进发,说到性能,目前高级语言中以性能作为特点的必然要想到C/C++,大量的3D游戏都是使用C/C++开发,再加上JS本身语法与C/C++差异并不大,于是开发者们便想着:如果将C/C++转换为JS代码,那不就可以做游戏了?从此,JS(被迫)多了一个子集asm.js

asm.js

asmjsJS和C/C++的差别主要有以下两点:

  • C / C++ 是静态类型语言,而 JS 是动态类型语言。
  • C / C++ 是手动内存管理,而 JS 依靠垃圾回收机制。

基于这两点,开发者们设计出了asm.js这项技术,它与常规js的区别就在于

  • 它的变量一律都是静态类型
  • 它取消垃圾回收机制

asm.js只支持两种数据类型:

32位带符号整数,64位带符号浮点数

asm.js 的类型声明有固定写法,变量 | 0表示整数,+变量表示浮点数

1
2
3
4
5
6
//asm.js 语法示例
var a = 1
var b = a|0
//原生js写法
var a = 1
var b = a

上述写法中,原生js写法只有在运行过程中变量b才能知道自己的类型,而asm.js写法中变量b则是在声明时就已经知道了自己的类型。

不就是加个类型嘛?这个我熟,TypeScript就是。。。。。。

[warn]虽然asm.js和TypeScript都是为JS添加了类型机制,但两者在本质上有着很大的区别:

  • TypeScript:它的本质只是为JS增加了修饰,最终还是将转为js执行,本质上并没有任何性能优化,其作用只在限制杂乱的js写法
  • asm.js:解释器和优化编译器为该子集有专门的优化,从编译层面对性能进行了优化和支持

(本篇对asm.js的相关语法不做特别详解,欲知后续,请等连载。。。)

Emscripten

Emscripten

上文提到asm.js基于其特殊的语法以及JIT为其特地准备的优化机制可以达到更高的性能,那么如果我们利用asm.js将C/C++代码重构必然是可以将原本用C/C++编写的程序和游戏移植到浏览器中,并且运行他们,但是使用js去重构C/C++项目无论是对精通js的程序猿还是精通C/C++的程序猿都是一个难题,那么asm.js难道就是一门使用条件极其严苛的鸡肋语言吗?

实际上asm.js本身与其说是一门编程语言,它更多的却像是一种中间代码语言(类似于字节码),它被设计出来的目的并不是有由程序猿人为的去编写相关程序,而是将C/C++代码编译为符合asm.js格式的代码,那么根据我们对编译原理的了解,在这个过程中有一个必不可少的工具——编译器

Emscripten就是与asm.js相辅相成的编译器

Emscripten 的底层是 LLVM 编译器,理论上任何可以生成 LLVM IR(Intermediate Representation)的语言,都可以编译生成 asm.js。 但是实际上,Emscripten 几乎只用于将 C / C++ 代码编译生成 asm.js。(太专一了,没对象的男孩子们好好学学)

1
C/C++ ⇒ LLVM ==> LLVM IR ⇒ Emscripten ⇒ asm.js

下载与安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 获取源码
git clone https://github.com/emscripten-core/emsdk.git

# 进入目录
cd emsdk

# 拉取最新的版本(第一次clone不需要)
git pull

# 下载安装最新的SDK工具.
./emsdk install latest

# 让当前用户可以使用最新的SDK (修改 .emscripten file)
./emsdk activate latest

# 修改环境变量
source ./emsdk_env.sh

Hello World !

首先创建halloWorld.cc文件

1
2
3
4
5
#include <iostream>

int main() {
std::cout << "Hello World!" << std::endl;
}

将其转换为asm.js

1
2
3
$ emcc helloWorld.cc
$ node a.out.js
Hello World!

emcc是Emscripten的编译命令,默认生成a.out.js文件

对于emcc的简单使用如下:

1
2
3
4
5
6
7
8
# 生成 a.out.js
$ emcc helloWorld.c

# 生成 helloWorld.js
$ emcc helloWorld.c -o helloWorld.js

# 生成 hello.html 和 hello.js
$ emcc helloWorld.c -o helloWorld.html

WebAssembly诞生

随着asm.js和Emscripten的使用,js在很长一段时间中得以以一种很高的性能去运行一些相对来说性能需求较高的功能,但技术终究是技术,永远没有终点,asm.js和Emscripten仅仅是对C/C++可以使用,但强大的开发者们将其思想提炼,最终形成了WebAssembly这项技术。

什么是WebAssembly?

WebAssembly并不是确切的指某一种编程语言,它只是一个字节码标准,它的运行依赖于JS虚拟机环境(JS引擎)

webAssembly可以说是对asm.js的一种抽象,它不再局限于将源代码转换成js代码,而是以字节码作为中间代码,只要各种源码最终被编译为符合webAssembly格式的字节码(**.wasm文件**),那么js虚拟机就可以载入并运行它。

WebAssembly与asm.js以及原生js的区别

WebAssembly与asm.js

两者的功能基本一致,就是转出来的代码不一样:asm.js 是文本,WebAssembly 是二进制字节码,因此运行速度更快、体积更小。从长远来看,WebAssembly 的前景更光明。

但是,这并不意味着 asm.js 肯定会被淘汰,因为它有两个优点:首先,它是文本,人类可读,比较直观;其次,所有浏览器都支持 asm.js,不会有兼容性问题。

WebAssembly与原生js

既然V8目前也会将js转换为字节码,那么两者是否等同呢?

其实两者依旧是不同的,由于js本身的语法特性如动态类型等,同样的操作其实对应的字节码与WebAssembly并不相同,相较之下,WebAssembly的体积更小,格式更为紧凑,也更加接近机器码,因此WebAssembly的运行要更加快,优势总结如下:

  • 大小:WebAssembly文件更小,通过网络请求速度更快
  • 解析:解码速度更快,本身经过编译,只要校验正确性完整性就可以
  • 编译和优化:WebAssembly本身已经进行过优化,所以优化更快
  • 重新优化(反优化):WebAssembly本身不需要反优化,因为编译器在有足够的信息在第一次运行时获得最正确的代码
  • 执行:执行速度更快,因为WebAssembly的指令非常接近机器码
  • 垃圾回收:手动控制垃圾回收,不支持自动回收,效率更高

从什么时候开始支持的

尽管我是在最近两年才开始在各大论坛和博客中了解到这个名词,但V8早已在5.0版本之前就已经宣布对WebAssembly开始实验性的支持,正如前文所说,V8在5.9版本之前实际上是没有采用解释器和字节码的,但他们却对WebAssembly进行了支持,开发者们为WebAssembly设计了一个单独的解码器(舔狗行为),这不得不让我们感到惊讶,原来对这项技术的支持早已在2016 年 3 月 15 日甚至更早就已经被js引擎开发者们提上了日程。

WebAssembly的使用

虽然目前很多语言尤其是Rust对WebAssembly的支持已经愈发强大,但作者当前水平有限,因而依旧采用C/C++作为示例

Emscripten环境安装

是的,你没有看错,就是那个可以将C/C++编译为asm.js的编译器,现在的它开始支持将C/C++编译为WebAssembly了(这就变心了?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git clone https://github.com/juj/emsdk.git
cd emsdk

# 在 Linux 或者 Mac OS X 上
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
# 如果在你的macos上获得以下错误
Error: No tool or SDK found by name 'sdk-incoming-64bit'
# 请执行
./emsdk install latest
# 按照提示配置环境变量即可
./emsdk activate latest


# 在 Windows 上
emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit

# 注意:Windows 版本的 Visual Studio 2017 已经被支持,但需要在 emsdk install 需要追加 --vs2017 参数。

生成HTML和JAVASCRIPT

  1. 创建hello.c文件

    1
    2
    3
    4
    5
    #include <stdio.h>

    int main(int argc, char ** argv) {
    printf("Hello World\n");
    }
  2. 在 emsdk 中搜索一个叫做 shell_minimal.html 的文件,然后复制它到刚刚创建的目录下的 html_template 文件夹。

    1
    2
    mkdir html_template
    cp ~/emsdk/emscripten/1.38.15/src/shell_minimal.html html_template
  3. 现在使用你的Emscripten编译器环境的终端窗口进入你的新目录, 然后运行下面的命令:

    1
    emcc -o hello.html hello.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html

    可以看到,Emscripten是通过WASM=1来控制生成asm.js或.wasm文件

  4. 上述代码会为我们生成一个含有“胶水代码”的html文件,直接打开,浏览器会为我们导入wasm文件并执行

更多示例或文档请前往WebAssembly官网

胶水代码:可以理解为将两种不同语言整合到一个文件中的代码,这里是指导入.wasm文件所使用的js代码

如何看待WebAssembly?

首先WebAssembly本身并不会取代JavaScript,它是作为JavaScript弥补性能不足的工具而被开发出来的,尽管其性能优越,但是对于在Web端领头地位占据已久的JavaScript而言,性能并不是一切,无论是JavaScript还是WebAssembly,我们的目标其实都是一致的,就是以更好更快的方式去完成页面的交互。

文章参考:

asm.js 和 Emscripten 入门教程

WebAssembly不完全指北

V8引擎官网