WebAssembly 系列(二)前世今生

1,497 阅读11分钟

上一篇文章中,我们详细介绍了前端的性能瓶颈,以及 webassembly 诞生元年之前,各路大神针对性能问题所做的各种探索和努力。

本篇,我们大致讲一下 webassembly 技术本身以及该技术目前的应用现状,好了,废话不多说,Action!

webassembly 是什么?

webassembly 不是一门新的语言,官方是这么给他定义的:

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

这里的定义可能给人一种感觉就是我认识每个字,但是组合在一起就。。。。

我们摘取里面的关键字挨个分析:

堆栈式虚拟机:

计算机理论里面有几种常见的计算模型:堆栈机,累加器机,寄存器机,说白了就是 CPU 在做运算的时候是基于哪种存储方式来定义的,顾名思义,堆栈机就是将数据存放在堆栈上,累加器就是将数据放在累加器上,寄存器机就将数据放在寄存器上,这里说数据有点儿不严谨,其实是操作数,三种计算模型各有优劣,看官方定义就知道了,wasm 选用了堆栈机来实现其目标。

堆栈机模型堆栈机,全称为“堆栈结构机器”,即英文的 “Stack Machine”。基于堆栈机模型实现的计算机,无论是虚拟机还是实体计算机,都会使用“栈”这种结构来实现数据的存储和交换过程。栈是一种“后进先出(LIFO)”的数据结构,即最后被放入栈容器中的数据可以被最先取出。

接下来,我们将尝试模拟堆栈机的实际运行流程。在这个过程中,我们会使用到一些简单的指令,比如 “push”,“pop” 与 “add” 等等。这里你可以把它们想象成一种汇编指令。大多数指令在执行时,都会从堆栈机的栈容器中取出若干个所需的操作数,然后根据指令所对应的功能,堆栈机会对取出的操作数进行一定的运算和处理。当这个过程结束后,若指令有需要返回的计算结果,这个值会被重新压入到栈容器中。

假设此时我们需要计算表达式 “1 + 2” 的值,那么通过栈机,这句表达式会以怎样的方式来执行呢?我们前面提到过,堆栈机中的栈容器,主要是作为程序执行时的数据存储和交换场所。那么对于上述表达式,编译器在实际进行编译时,假设在没有使用任何优化策略的情况下,通常会生成类似如下的这样几条指令。

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

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

可以看到,目前栈容器中存放有通过前两条 push 指令压入的操作数 “1” 和 “2”。接下来,堆栈机继续执行第三条 “add” 指令。

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

当全部指令执行完毕后,在栈容器中,会存放有表达式 “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 指令进行 SSA(Static Single Assignment Form,静态单赋值形式)变换,可以保证即使是在堆栈机模型下,Wasm 代码也能够有着较好的执行性能。而堆栈机模型本身长短适中的指令长度,确保了 Wasm 二进制模块能够在相同体积下,拥有着更高密度的指令代码。

Wasm 虚拟指令集

到这里,我们已经知道了 Wasm 是一种基于堆栈机模型设计的 V-ISA 指令集。那下面就让我们来一起看看它的真实面目。如下所示,是一段标准的 Wasm 指令。这段指令的功能与我们之前在介绍三种计算模型时所使用的例子一样。

i32.const 1
i32.const 2
i32.add

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

同样的,堆栈机在实际执行这条指令前,也会首先检查当前的栈容器顶部是否含有至少两个 i32 类型的值。可以看到,上述这段 Wasm 指令的执行方式,与我们在介绍堆栈机模型时,所采用的那个案例中的指令执行流程完全一样。相信此时的你,一定会对本文开头 “Wasm 是什么?” 这个问题的答案有了新的认识。

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

高级语言的编译目标

Wasm 虽然有着类似汇编语言的这种“助记符”形式,但在大多数情况下,它仅被作为诸如 C/C++ 等高级编程语言的最终编译目标。

编译器会自动处理从这些高级语言源代码到 Wasm 二进制指令的转换过程。而这也正如我们在开头所提到的那样,官方声称的 ”Wasm 被设计成为一种编程语言的可移植编译目标”。

所以想徒手写这些类汇编语言的同学,可以放弃了(想装 13 的除外),大家大学的时候应该都学过汇编语言,曾经我用了很长时间才研究明白如何用汇编来实现 1 + 1 = 2;就像现在大家都写 ts 然后交给 webpack 编译一样,而js 就是 ts 的编译目标。

另外还有个关键词就是可移植,这无疑会大大增强 webassembly 的应用场景和灵活性,其实只要实现了 webassembly 的虚拟机,那么 wasm 代码可以运行在任何环境里。

转 wasm 的高级语言

WebAssembly支持不断发展。目前,以下语言支持它:

  • C / C ++-通过EmScripten或其他基于LLVM的最小工具链提供了很好的支持(可用于生产环境)

  • Rust-WebAssembly是受官方支持的目标,周围有非常活跃的社区。

  • Go-现在已将WebAssembly作为正式但实验性的目标来支持

  • C#-通过Blazor具有实验支持,但是当前需要将.NET运行时嵌入Wasm。最近发布了预览版,Blazor被Microsoft正式用作实验技术。

  • D-D的“ betterC”子集可以通过LDC(LLVM编译器)编译为WebAssembly。

  • TypeScript-通过AssemblyScript,实验性强,但势头强劲。

  • Java-通过TeaVM或Bytecoder

  • Haxe-刚刚宣布支持

  • Kotlin-Kotlin / Native 0.4通过WebAssembly和TeaVM获得了实验支持

  • Python-Pyodide是WebAssembly的Python移植,其中包括科学Python堆栈的核心软件包(Numpy,Pandas,matplotlib)。

  • PHP-实验性的,但具有有效的原型

  • Perl-WebPerl是Perl二进制文件到WebAssembly的端口,允许您在Web上运行Perl脚本。

  • Scala-使用Emscripten编译器

  • Ruby-通过run.rb项目

  • Swift-使用SwiftWasm,目前正在开发中

目前本人比较推荐 学习的是 AssemblyScript 和 Rust(笔者用这个),AssemblyScript是基于 TypeScript开发的wasm 专用语言,对于熟悉 ts 的前端 同学来说比较友好。

Rust 跟 wasm 师出同门(跟 flutter 与 dart 关系类似,但是比 dart 应用场景要多很多),是门很年轻的语言,但是在 Stack Overflow 上连续五年被评为程序员最喜欢的语言,其有一个很大的优势,就是编译成 wasm 之后体积特别小,这对于需要通过 https 加载 wasm 模块再运行的浏览器而言是一个很大的优势。

当然对于其他语言生态的程序员,大家熟悉啥语言,就尝试啥语言就好,毕竟最难的是迈出第一步。

wasm 的应用场景

在浏览器中

  • 更好的让一些语言和工具可以编译到 Web 平台运行。

  • 图片/视频编辑。

  • 游戏:

    • 需要快速打开的小游戏
  • AAA 级,资源量很大的游戏。

  • 游戏门户(代理/原创游戏平台)

  • P2P 应用(游戏,实时合作编辑)

  • 音乐播放器(流媒体,缓存)

  • 图像识别

  • 视频直播

  • VR 和虚拟现实

  • CAD 软件

  • 科学可视化和仿真

  • 互动教育软件和新闻文章。

  • 模拟/仿真平台(ARC, DOSBox, QEMU, MAME, …)。

  • 语言编译器/虚拟机。

  • POSIX用户空间环境,允许移植现有的POSIX应用程序。

  • 开发者工具(编辑器,编译器,调试器…)

  • 远程桌面。

  • VPN。

  • 加密工具。

  • 本地 Web 服务器。

  • 使用 NPAPI 分发的插件,但会受限于 Web 安全协议,可以使用 Web APIs。

  • 企业软件功能性客户端(比如:数据库)

脱离浏览器

  • 游戏分发服务(便携、安全)。

  • 服务端执行不可信任的代码。

  • 服务端应用。

  • 移动混合原生应用。

  • 多节点对称计算

目前比较大的应用商业化案例有如下几个:

  • Google Earth

  • Auto CAD

  • Unity、Unreal 游戏引擎

  • Figma(国外 UI 的最爱,替换 sketch 指日可待)

另外 wasm 因为自带沙箱属性,所以 docker 的创始人才会感叹如果 wasm 早面市十年,他就不会创办docker 了。

服务端的应用大家可以查看 second state 这个网站,他们主要用 wasm 来进行服务器方面的开发。