WebAssembly 入门篇

·  阅读 369

为什么会有 WebAssembly 这样一门技术?

JavaScript 的发展困境:

  • web 应用规模的急速增长:随着移动互联网的发展和各种形式经济活动的不断展开,运行在浏览器中的各类 Web 应用,它们的体积与复杂性随着时间的推移在不断发展。但与日益庞大和复杂化的 Web 应用相比,浏览器对自身性能的优化可谓是举步维艰。不难预见,当“复杂化”与“性能优化”的速度之比不断变大时,迟早有一天,浏览器会再也无法支撑起这些庞大 Web 应用的运行。
  • JavaScript 的弱类型之殇:JavaScript 是一个“动态类型”的编程语言。在实际编码过程中,我们不需要为每一个变量指定对应类型。变量具体类型的推导过程,会被推迟到代码的实际运行时再进行。JavaScript 这种动态类型语言所独有的特性,在某种程度上相较于静态类型语言而言,会带来额外的运行时性能开销。在现代的 JavaScript 引擎中,尽管可以使用诸如 JIT 等技术来提高代码的执行效率,但在实际使用中,如果代码执行没有遵守 JIT 优化路径中特定 Guard 的要求,“去优化”的过程,也同样会影响引擎的整体执行效率。

Web 前端正变得越来越开放。如今,我们不仅能够直接使用 HTML、JavaScript来编写各类跨端应用程序,WebAssembly 的出现更能够让我们直接在 Web 平台上,使用那些业界已存在许久的众多优秀的 C/C++ 代码库。

WebAssembly 还能让 Web 应用具有更高的性能,甚至让 Web 应用能够与原生应用展开竞争。2019 年 12 月,W3C 正式宣布,Wasm 将成为除现有的HTML、CSS 以及 JavaScript 之外的第四种,W3C 官方推荐在 Web 平台上使用的“语言”。

下面是WebAssembly的一些应用场景:

WebAssembly 的发展历程

  • 最初的尝试—— NaCl与PNaCl

NaCl(Native Client) 是由 Google 在 2011 年于 Chrome 浏览器中发布的一项技术,该技术旨在提供一个沙盒环境,可以让基于 C/C++ 语言编写的 Native 应用,安全地运行在浏览器中。

如下图所示,一个标准 NaCl 应用的组成结构,与普通的 JavaScript Web 应用十分类似。NaCl 模块作为应用的一部分,主要用来进行复杂的数据处理和运算,JavaScript 则负责处理应用与外部用户的交互逻辑。NaCl 实例与 JavaScript 代码之间可以通过“订阅 / 发布”模型,来互相传递消息。

理想虽好,但现实却存在着很多问题。通常,一个 NaCl 模块文件需要在开发者本地进行编译,然后才能够在浏览器中使用。而本地编译的模块文件通常仅含有架构相关的代码,因此没有办法直接在其他类型的系统中使用。

一个完整的 NaCl 应用,在分发时需要提供支持多个架构平台(X86_32 / X86_64 / ARM等)的模块文件。浏览器在实际使用时,会根据当前系统的具体架构类型,来动态地选择,对应合适的模块文件进行使用。

不仅如此,由于 NaCl 模块“平台依赖”的特殊性,因此 NaCl 模块进行分发的过程,仅能够在 Chrome Web Store 中进行。另一方面,如果你想要将已经存在的 C/C++ 代码库编译至 NaCl,并在浏览器中使用,你还需要通过名为 Pepper 的库来对这些代码进行重写。Pepper 提供了很多包装类型,以及用于和浏览器进行交互的 API,比如“PP_Bool”等。这些 API 和特殊类型可以便于整合传统 C/C++ 代码与 Web 浏览器的沙盒环境。

鉴于 NaCl 存在的“平台依赖”问题,Google 在后期又推出了名为 PNaCl 的技术。这里名字中多出来的 “P”代表着“Portable”,也就是“可移植”的意思。PNaCl 采用了不一样的生命周期,下图可以看到,相较于 NaCl 模块直接包含有平台架构相关的代码,PNaCl 将源 C/C++ 代码编译到一种中间代码。这些中间代码会在浏览器实际加载这个 PNaCl 模块时,再被转换为对应的平台相关代码。因此,对于 PNaCl模块而言,分发的过程变得更加简单,且不用担心移植性的问题。

不过,即使是对于 PNaCl 这类“可移植性”已经不再成为问题的技术而言,它们的面前还有很多“大山”难以逾越。比如:“需要使用 Pepper 重写 C/C++ 代码,标准较为封闭、仅 Chrome 浏览器支持”等等。

现在,如果你再次回到 NaCl/ PNaCl 在 Google 的官方文档网站,你会发现如下这样一段声明。WebAssembly 将会作为新一代的技术,接替并继续传承 Google 赋予给 NaCl / PNaCl 的使命。

  • WebAssembly的前身—— ASM.js

Mozilla 于 2013 提出 ASM.js,ASM.js 的设计目标也是为了能够在 JavaScript 语言之外,为“构建更高性能的 Web 应用”这个目标,提供另外一种实现的可能。

ASM.js 是 JavaScript 的一个严格子集。它是一种可用于编译器的目标语言,低层次且高效。该目标语言有效地为内存不安全语言(如 C/C++)描述了一个沙盒虚拟机运行环境。静态和动态验证相结合的方式,使得 JavaScript 引擎能够使用 AOT 等优化编译策略来验证 ASM.js 代码。

第一,ASM.js 是 JavaScript 的严格子集。这也就意味着,对于一段 ASM.js 代码,JavaScript 引擎可以将它视作普通的 JavaScript 代码来执行,这便保障了 ASM.js 在旧版本浏览器上的可移植性。

第二,ASM.js 使用了“Annotation(注解)”的方式来标记代码中包括:函数参数、局部 / 全局变量,以及函数返回值在内的各类值的实际类型。当 JavaScript 引擎满足一定条件后,便会通过 AOT 静态编译的方式,将这些被Annotation 标记的 ASM.js 代码,编译成对应的机器码并加以保存。当 JavaScript 引擎执行这段 ASM.js 代码时,便会直接使用先前已经存储好的机器码版本。因此,引擎的性能会得到大幅的提升。

WebAssembly 是一门新的编程语言吗?

“WebAssembly(缩写为 Wasm)是一种基于堆栈式虚拟机二进制指令集。Wasm 被设计成为一种编程语言的可移植编译目标,并且可以通过将其部署在 Web 平台上,以便为客户端及服务端应用程序提供服务”。

  • 堆栈机模型

堆栈机是一种常见的计算模型。基于堆栈机模型实现的计算机,无论是虚拟机还是实体计算机,都会使用“栈”这种结构来实现数据的存储和交换过程。接下来,将尝试模拟堆栈机的实际运行流程。

假设此时需要计算表达式 “1 + 2”的值,那么通过栈机,这句表达式会以怎样的方式来执行呢?编译器在实际进行编译时,假设在没有使用任何优化策略的情况下,通常会生成类似如下的这样几条指令。

如上图所示,按照指令从上到下的执行顺序放在左侧。堆栈机中栈容器的当前状态放置在右侧。可以看到,此时的栈容器为空,内部没有任何数据。下面,堆栈机开始执行第一条指令 “push 1”。push 指令会将紧随其后出现的操作数直接压入栈中。当该指令执行完毕后,此时栈容器的状态如下图所示。

此时,栈容器的栈底存放着通过第一条 push指令压入的操作数 “1”。以同样的方式,堆栈机继续执行第二条指令 “push 2”。该条指令执行完毕后,栈容器的状态如下图所示。

接下来,堆栈机继续执行第三条 “add”指令。执行这条指令需要两个操作数,因此在执行指令时,堆栈机会首先检查当前的栈容器,看其中存放的元素数量是否满足“大于或等于 2 个”。如果这个条件成立,堆栈机会直接从栈容器的顶部取出两个操作数,然后将它们直接相加,所得到的结果会被再次压入到栈容器中。当最后一条 add 指令执行完毕后,此时栈容器的状态如下图所示。

当全部指令执行完毕后,在栈容器中,会存放有表达式 “1 + 2”在经过堆栈机求值后的结果值。

堆栈机使用栈结构作为数据的存储与交换容器,由于其“后进先出”的特性,使得我们无法直接对位于栈底的数据进行操作。因此在某些情况下,机器会使用额外的指令来进行栈数据的交换过程,从而损失了一定的执行效率。但另一方面,堆栈机模型最为简单且易于实现,对应生成的指令代码长短大小适中。

  • ISA与V-ISA

通常来说,对于可以应用在诸如 i386、X86-64 等实际存在的物理系统架构上的指令集,我们一般称之为 ISA(Instruction Set Architecture,指令集架构)。而对另外一种使用在虚拟架构体系中的指令集,我们通常称之为 V-ISA,也就是 Virtual的 ISA。对这些 V-ISA 的设计,大多都是基于堆栈机模型进行的。而 Wasm 就是这样的一种 V-ISA。

Wasm 之所以会选择堆栈机模型来进行指令的设计,其主要原因是由于堆栈机本身的设计与实现较为简单。快速的原型实现可以为 Wasm 的未来发展预先试错。另一个重要原因是,借助于堆栈机模型的栈容器特征,可以使得 Wasm 模块的指令代码验证过程变得更加简单。

  • Wasm虚拟指令集

到这里,我们已经知道了 Wasm 是一种基于堆栈机模型设计的 V-ISA 指令集。那下面就让我们来一起看看它的真实面目。如下所示,是一段标准的 Wasm 指令。

i32.const 1
i32.const 2
i32.add
复制代码

前两条指令使用了 “i32.const”,这个指令会将紧随其后的立即数作为一个 i32 类型,也就是 32 位整数类型的值,压入到堆栈机的栈容器中。最后一条指令 “i32.add”,会取出位于栈容器顶部的两个 i32 类型的值,并相加,然后再将计算结果重新放回到栈容器中。堆栈机在实际执行这条指令前,会首先检查当前的栈容器顶部是否含有至少两个 i32 类型的值。

类比汇编语言与机器码。这里的 “i32.const”与“i32.add”,其实都是 Wasm 这个 V-ISA 指令集中,各个指令所对应的文本助记符(mnemonic)。实际当这些助记符被编译到 Wasm 二进制模块中时,会使用助记符所对应的二进制字节码(一般被称为 OpCode,可以简单地将其理解为一些二进制数字),并配合一些编码算法来压缩整个二进制模块文件的体积。

Wasm 虽然有着类似汇编语言的这种“助记符”形式,但在大多数情况下,它仅被作为诸如 C/C++ 等高级编程语言的最终编译目标。编译器会自动处理从这些高级语言源代码到 Wasm 二进制指令的转换过程。而这也正如官方声称的 ”Wasm 被设计成为一种编程语言的可移植编译目标“。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改