「如果2008年的时候,WASM 和 WASI(WebAssembly System Interface, WASM 系统接口)这两个东西已经存在了的话,我们就没有必要创立 Docker 了。
在服务器上运行 WebAssembly 是计算的未来,目前缺少的就是一个标准的系统接口,希望 W
A
SI 能够弥补上这块缺失的拼图。
」——Docker 创始人 Solomon Hykes
2017 年发布的 WebAss embly 技术曾是关注的焦点,发布之初即被寄予厚望,被视为 JS 的替代者,8-15倍的性能提升让人兴奋不已。 而随着 Docker 创始人在 2019 年 3 月发布的一条 Twitter 又让人畅想起了其在服务端的应用。 Java 所提出的「一次编译,多处运行」似乎将进一步实现。 时间来到 2024 年,WebAssembly 从网红跌落神坛,技术推广并不成功,90% 以上的场景不需要 WebAssembly,但其技术发展却在持续成熟。 脱离技术炒作的喧嚣,本文作者从WebAssembly 的前世今生说起,深入介绍了这门面临七年之痒的潮流技术的发展历程与现状。
作者 | 段鱼
出品丨腾讯云开发者
WebAssembly,前身技术来自 Mozilla 和 Google Native Client 的 asm.js, 首次发布于 2017 年 3 月。并于 2019 年 12 月 5 日正式成为 W3C recommendation,至此成为与 HTML、CSS 以及 JavaScript 并列的 Web 领域第四类编程语言。
在 Web 领域,已经有 JavaScript 这样的利器,而 WebAssembly 则是打开新世界的大门。WebAssembly 并 不是要取代 JavaScript,而是要在图形图像处理、3D 游戏、AR/VR 这些应用领域开疆拓土。如今的现代浏览器已经越发朝着微型」第二操作系统「发展,人们希望在浏览器内能完成更多的事情,而 WebAssembly 作为 Web 端高性能应用的基石,正在让更多的应用场景在浏览器内变为现实。
除了在浏览器内实现高性能应用,WebAssembly 也可以脱离 Web 端在搭载了不同硬件和操作系统的各个平台运行,进一步实现当年 Java 所期望的「一次编译,多处运行」。 WebAssembly 在服务端可用于微服务平台、无服务平台、 第三方插件 系统等场景。
WebAssembly 的前世今生:从 Mozilla 说起
1.1 一家伟大的互联网企业
说起 WebAssembly,那就必须从一家没落而又伟大的互联网公司说起,它就是火狐浏览器的开发者 Mozilla。 Mozilla 的前身是大名鼎鼎的网景公司(Netscape),也就是 JavaScript 的开发者。 从做浏览器起家一路坎坷至今,Mozilla 最近更是频频传出裁员风波,其根源依然是没有找到太好的盈利点。 作为互联网开源社区的领跑者,Mozilla 在技术上的成就远高于其在商业领域。 除了 JavaScript 和 Filefox,Mozilla 还留下了 Rust 、HTML5、MDN(Mozilla Developer Network)以及 asm.js 这些引领互联网行业发展的重要基石。
1.2 脑洞大开的想法:浏览器里跑 C++
2012年 Mozilla 的工程师在研究 LLVM 时,突然脑洞大开提出了一个想法: 类似游戏引擎这样的高性能应用大多都是 C/C++语言写的,如果能将 C/C++转换成 JavaScript ,那岂不是就能在浏览器里跑起来了吗? 如果可以实现,那么浏览器是不是也就可以直接跑 3D 游戏之类的 C/C++应用? 于是 Mozilla 成立了一个叫做 Emscripten 的编译器研发项目,Emscripten 可以将 C/C++代码编译成 JavaScript,但不是普通的 JS,而是一种被特殊改造的 JS,其被命名为 asm.js。
Emscripten 的官方描述是:
Emscripten is a toolchain for compiling to asm.js and WebAssembly, built using LLVM, that lets you run C and C++ on the web at near-native speed without plugins.
中文译文:
Emscripten 是一个基于 LLVM 的将 C/C++编译到 asm.js 和 WebAssembly 的工具链,它可以让你在 Web 上以接近原生的速度运行 C/C++而不需要任何插件。
如下图所示: 实际上,不只是 C/C++代码,只要能转换成 LLVM IR 的语言,都可以通过 Emscripten 转换成 asm.js。
1.3 另一次失败的尝试:Google Native Client
Google 在很早之前也一直致力于研究如何让 C/C++能够在 Chrome 里运行起来,并在2009年的安全领域顶级会议 IEEE Symposium on Security and Privacy 发表了 Google 的技术方案 NaCl(Google Native Client)以及 PNaCl(Portable Google Native Client)。 NaCl 的本质也是一种沙盒技术,使用工具链编译后的 C/C++代码能够以接近原生应用的速度在 web 端运行,也可以与 JS 和 webapi 进行交互。 NaCl 在安全这块做了大量的设计,其使用了内外双层沙盒,并利用 x86 内存分段机制来隔离内存,甚至还用上了静态代码分析技术来做沙盒里运行的程序进行检查。
然而在经过了8年的挣扎后,在2017 年5月30日 Google 宣布弃用 NaCl。 其根 本原因是 NaCl 这套方案只有自家的 Chrome 愿意配合支持,所以压根就不具备跨浏览器运行的能力。最终 Chrome 与 Mozilla 达成一致,共同推进 WebAssembly 方案,Chrome 也直接用 WebAssembly 替换掉了 NaCl。
asm.js:WebAssembly 的前身,一种更快的 JS
2.1 C++转换 asm.js 示例
一般来说,asm.js 并不是直接编写的,而是一个面向 JS 编译器的中间产物。 例如以下的 C++ 代码:
//计算i+1
intf(int i){
return i + 1;
}
//计算字符串长度
size_tstrlen(char *ptr) {
char *curr = ptr;
while (*curr != 0) {
curr++;
}
return (curr - ptr);
}
使用 Emscripten 转换后,生成的 JS 代码如下:
functionf(i) {
i = i|0;
return (i + 1)|0;
}
functionstrlen(ptr) {
ptr = ptr|0;
var curr = 0;
curr = ptr;
while ((MEM8[curr>>0]|0) != 0) {
curr = (curr + 1)|0;
}
return (curr - ptr)|0;
}
可以看到这种生成的 JS 跟普通 JS 还是区别很大的,就像刚才我们所说, 程序员不直接编写 asm.js 代码,这些看起来怪异的语法都是为了配合编译器生成更高效的机器码 。比如在 asm.js 里反复出现的"按位或"操作,其目的是将原来 JavaScript 里的 double 类型计算转为整形运算(CPU进行整形运算的速度快于浮点型)。而这里被命名为 MEM8 的数组实际上充当了"堆"的作用。如果只是作为使用者可以不用深究这些优化的具体实现,直接使用 Emscripten 来帮助我们完成这一转换过程即可。
2.2 asm.js 为什么比原生 JavaScript 快?
由于 asm.js 在浏览器中运行,其性能在很大程度上也取决于浏览器和 JS 引擎的优化支持。2015年6月,Microsoft Edge 也开始加入了对 asm. js 的支持。为了直观展示 asm.js 所带来的的性能提升,微软发布了一个叫做"Chess Battle"的 demo。Chess Battle
让两个版本的开源象棋 AI 对战,其中一个用 C 实现然后转成 asm.js,另外一个用原生JS实现。如下图所示,每个走棋回合限制为200毫秒,其中 asm.js 版本的 AI 因为可以在每个回合进行更多的评估运算(用于决定走棋策略),胜率获得了极大提升。
asm.js 运行的快慢取决于不同的测试用例、运行硬件、浏览器引擎优化程度等,一般来说我们可认为 asm.js 能达到原生 C/C++运行速度的50%, 有些场景下甚至能持平 Clang 编译的 C/C++用例。asm.js 运行比原生 js 快,那么它如此高效的原因是什么呢?阮一峰在他的一篇博客里写到的结论是:
一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。
这篇博客应该是对很多人造成了误导,具体错误在于:
首先,"跳过语法分析,直接生成汇编"是不存在的,语法分析是编译中不可缺少的一环节,asm.js 跟原生 JS 的编译运行过程是一致的。
其次,WebGL 作为一个图形 api 和 asm.js 技术可以说是没有任何直接关系,原生JS也调用 WebGL 来实现 GPU 硬件加速。
最后,也是最离谱的一点,WebGL 通过 GPU 执行 asm.js ?不管是 asm.js、原生 JavaScript 还是 WebAssembly 其编译产物都是 CPU 机器码而不是 GPU 机器码。而且 WebGL 只是一个图形渲染 api,就算是把J S 编译到 GPU 也需要类似 CUDA/OpenCL 这些通用计算 api 来支持。最新的 WebGPU 同时支持了图形和通用计算,这倒是目前 Web 端在 GPU 里"执行 JS"的可行方法。
先抛开 JavaScript 不谈,我们可以思考一下,对于任何一门编程语言来说决定其运行快慢的根源是什么呢?我认为用一句话来总结就是: 代码运行的快慢,从硬件层面上看,直接取决于生成的机器码所需时钟周期的总和。从编程语言层面上看,取决于编译后的产物在运行时有多少"动态决议"。
例如,弱类型语言比强类型语言慢,是因为编译时类型是不确定的,需要运行时进行额外的型别推导,这就是"动态决议";
例如,C++ 里虚函数比普通函数开销大,是因为编译时函数地址是不确定的。普通函数编译后生成的跳转目的地是一串固定的地址,而虚函数的跳转地址是在运行时从 CPU 的寄存器里读取的,这也是"动态决议",编译后的机器码多了一条寄存器取值指令;
类似的场景还有 GC 机制、模板编程、JIT 优化等等,归根结底就是如果在编译时候能完成更多事情,那么生成的机器码运行周期就越短,代码也就运行地越快。asm.js 在减少运行时的"动态决议」这里所做的工作,wiki 原文如下:
Much of this performance gain over normal JavaScript is due to 100% type consistency and virtually no garbage collection.
可翻译为:
与原生 JavaScript 相比,这里性能提升的主要原因是100%的类型一致性以及几乎没有(自动的)垃圾回收机制。
简而言之就是,asm.js 的实现去掉大部分的自动 GC 机制,然后改成了强类型语言,编译器能够更大程度地进行优化,这才是 asm.js 能比普通 JS 运行更快的原因。 在 asm.js 里不再支持除了浮点和整形之外的类型,内存的开辟和释放也需要代码手动进行处理。部分引擎甚至还可以以 AOT 或者 JIT 的形式运行 asm.js。关于 asm.js 的原理,在微软的文档里也有一段更加详细的描述:
Asm.js is a strict subset of JavaScript that can be used as a low-level, efficient target language for compilers. As a sublanguage, asm.js effectively describes a sandboxed virtual machine for memory-unsafe languages like C or C++. A combination of static and dynamic validation allows JavaScript engines to employ techniques like type specialized compilation without bailouts and ahead-of-time (AOT) compilation for valid asm.js code. Such compilation techniques help JavaScript execute at 「predictable」 and 「near-native」 performance, both of which are non-trivial in the world of compiler optimizations for dynamic languages like JavaScript.
这段话从编译器优化的角度对 asm.js 原理描述地非常贴切了,比较难准确翻译,大概释义如下:
asm.js 是 JavaScript 的一个严格子集,是一种面向编译器的底层且高效的目标语言。作为一种子语言,asm.js 高效地为类似 C/C++这样的内存不安全语言描述了一个沙盒虚拟机。静态验证和动态验证的结合允许 JavaScript 引擎对有效的 asm.js 代码使用型别特化编译和提前(AOT)编译等技术。这样的编译技术可以帮助 JavaScript 具有"可预见性"和「接近原生」的性能表现,这两种特性在 JavaScript 这样的动态语言编译器优化中是非常重要的。
其中"bailouts"应该是微软这个 JS 编译器里的专用名词,没有特别合适的翻译。"predictable"可理解为「更少的动态决议」。asm.js 目前看已经是过时的技 术,并非本文的重点也不再展开继续讨论,如果想继续了解 JavaScript 编译优化的实现细节,读者可参阅文献的内容自行研读。
WebAssembly:绕过 JS 直接生成机器码
Asm.js 的思路是将一种编程语言转换成另外一种编程语言,输出的还是 JS 代码。 那么这里你肯定也想到了,我们为什么不能绕过 JavaScript ,将 C/C++代码直接转成浏览器可以识别的更底层的语言呢? 这就是由 Asm.js 衍生出的WebAssembly 技术。
3.1 WebAssembly 是什么?
如上图所示,为了能便于程序员阅读和编辑 WebAssembly,源码除了被编译成二进制外还会生成一份文本文件。左边红色部分是 C++源码,中间紫色部分是文本格式的 .Wat 文件的内容,右边蓝色部分是 .wasm 文件的内容。
多数情况下,人们把Wasm定义成 Web 上的编程语言,认为这是一个前端编程技术。其实这里有一些的误解,首先 Wasm 并不是一个新的"编程语言",没有人会手写 .wasm 文件来进行编程。 WebAssembly 有一套完整的语义,但作为开发者并不需要去了解它,开发者依然可以继续使用自己熟悉的编程语言,由各个语言的编译器将其编译成 Wasm 格式后运行在浏览器内置的Wasm虚拟机中 ,我认为 Wasm 更倾向于是一个应用在web场景中的编译领域新技术。其次,Wasm 也并非只能运行在浏览器内,设计者对其抱有更加远大的宏图大业,这部分我们将在后面 Wasm 容器化这里继续展开讨论。
Mozzila 官方对 WebAssembly 的描述为:
WebAssembly is a new type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.
可翻译为:
WebAssembly 是一种可以在现代浏览器中运行的新型代码——它是一种低级的类似汇编的语言,具有紧凑的二进制格式,运行起来具有接近 原生的性能,其为 C/C++、C#和 Rust 等语言提供了一个编译目标,以便它们可以在 Web 上运行。 它还被设计为与 JavaScript 一起运行,允许两者一起工作。
通过这段描述已经可以对 WebAssembly 有一个初步认识,我们再进一步给它拆开来看:
首先,WebAssembly 是一门新的编程语言,它于2019年12月5日正式成为与 HTML、CSS 以及 JavaScript 并列的 Web 领域第四类编程语言。
其次,WebAssembly 是"汇编语言"而不是高级语言,程序员不直接编写 WebAssembly 代码,而是通过特殊的编译器将高级语言转换成 WebAssembly 代码。
再次,WebAssembly 是预处理过后的二进制格式,它实际是一个 IR(Intermediate Representation)!类似 Java 的 ByteCode 或者 .Net 的 MSIL/CIL。
最后,WebAssembly 是 Web 上的语言,这意味着主流的浏览器可以读取并且执行它。
最后简单总结,程序员依然还是编写高级语言,然后通过「特殊的编译器」生成 WebAssembly 二进制代码,最终 WebAssembly 代码再被一个嵌入在浏览器里的"特殊的虚拟机"执行。 这就是 WebAssembly 的全部工作过程。
3.2 为什么需要 WebAssembly?
在 Web 领域,我们已经有了 JavaScript 这样利器,但美中不足的是 JavaScript 的性能不佳,即使可以通过第二章里提到的各种编译优化来解决一部分问题,但在类似图形图像处理、3D 游戏、AR、VR 这些高性能应用的场景下,我们似乎任然需要一个更好的选择。
「快」是相对的,目前我们可以认为在运行速度上:原生 C/C++ 代码 > WebAssembly > asm.js > 原生 JavaScript 。其中 WebAssembly 比 asm.js 要快的原因在于:
1. WebAssembly 体积更小,JavaScript 通过 gzip 压缩后已经可以节约很大一部分空间,但 WebAssembly 的二进制格式在被精心设计之后可以比 gzip 压缩后的 JavaScript 代码小 10-20% 左右 。
2. WebAssembly 解析更快,WebAssembly 解析速度比 JavaScript 快了一个数量级 ,这也是得益于其二进制的格式。除此之外,WebAssembly 还可以在多核CPU上进行并行解析。
3. WebAssembly 可以更好利用 CPU 特性,之前我们说到 asm.js 可以通过各种「奇技淫巧」来编译优化,但其还是受限于 JavaScript 的实现。而 WebAssembly 可以完全自由发挥,使得其可以利用更多 CPU 特性,其中例如:64位整数、加载/存储偏移量以及各种 CPU 指令。在这一部分,WebAssembly 能比 asm.js 平均提速 5% 左右 。
4. 编译工具链的优化,WebAssembly 的运行效率同时取决于两部分,第一个是生成代码的编译器,第二个是运行它的虚拟机。WebAssembly 对其编译器进行了更多的优化,使用 Binaryen 编译器代替了 Emscripten, 这部分所带来的的速度提升大约在 5%-7% 。
当然,速度上的提升并不是全部。WebAssembly 的意义在于开辟了一个新的标准,不再拘泥于 JavaScript 而是直接面向跟底层的机器码。用任何语言都可以开发 WebAssembly,而 WebAssembly 又可以高效运行在任何环境下,这也是 Mozilla 的程序员对 WebAssembly 抱有的最远大的宏图大业。文章将在第六章对 WebAssembly 在非 Web 端的应用继续展开讨论。
3.3 WebAssembly 与 JavaScript 运行性能详细对比
关于 WebAssembly 的性能,整体上我认为可以描述为「很快,但是不够快」。也就是说, 我们期望它比 JavaScript 快非常多,快个10倍或者8倍,但实际上只能快一点点,大概也就是不到2倍左右 ,而且在不同的测试场景下差异可能会很大。也许你会说100%的性能提升已经很高了,但实际上这也许不能说服大量开发人员完全转向一个崭新的有学习成本的技术。
Zaplib(一个高性能 Web 框架)的工程师从最大性能和标准性能两方面对 WebAssembly 与 JavaScript 性能进行更详细的对比,结论如下:
3.3.1 最大性能(尽可能"奇技淫巧"地使用 JS)
在最大性能上,特殊编写的原生 JS 是可以跟 Wasm 大致持平的。其原因在于JS可以通过 ArrayBuffer 来模拟成一个"memory managed language":
1. 可以尽可能避免掉自动 GC 的额外开销。
2. 可以对数据的局部性(cache locality)进行优化来提升缓存命中,从而提升数据读写的效率。(缓存局部性对数组的性能很重要!)
3. 当你尽可能避免掉其它开销,只使用循环、局部变量、算术、函数调用的时 候,原生 JS 会非常快。
举个例子如下,这是一个计算多个2维向量平均长度的 TS 函数
// Unoptimized Typescript
type Vec2 = { x: number, y: number };
functionavgLen(vecs: Vec2[]): number{
let total = 0;
for (const vec of vecs) {
total += Math.sqrt(vec.x*vec.x + vec.y*vec.y);
}
return total / vecs.length;a
}
这是使用了 ArrayBuffer 替换数组了实现:
// Optimized Typescript, using ArrayBuffers
functionavgLen(vecs: ArrayBuffer): number{
let total = 0;
const float64 = newFloat64Array(vecs);
for (let i=0; i<float64.length; i += 2) {
const x = float64[i];
const y = float64[i+1];
total += Math.sqrt(x*x + y*y);
}
return total / (float64.length / 2);
}
在示例中,ArrayBuffer 每 16 位存储一个二维向量,前 8 位是向量 x,后 8 位是向量 y。后者代码的性能会远高于前者,具体细节有兴趣可以参考(
https://zaplib.com/docs/blog_ts ++.html)。总而言之就是,可以通过 JS 的 ArrayBuffer 来手动管理 JS 内存,尽量避免掉性能开销大的地方,剩下的普通指令的执行跟 Wasm 并无本质差异。除此之外,浏览器里的 JS 相比 Wasm 在某些方面甚至还具有优势:
1. JS 可以访问一些零拷贝(zero-copy)的方法。例如 TextEncoder 和 FileReader.readAsArrayBuffer,而 Wasm 还需要额外再进行一次内存拷贝。
而 Wasm 相比 JS 的优势在于:
2. SIMD 加速。SIMD.js 的 API 已经被弃用,取而代之的是 Wasm 的 SIMD 实现。
3. 前置的编译优化。
3.3.2 标准性能(正常使用编程语言)
对于实际情况而言,用标准的JS的进行性能对比才是有意义的,原因在于:
1. 代码的编写复杂度和可维护性也是很重要的,"奇技淫巧"并不适合生产工作中使用。
2. 代码工程会依赖大量第三方库,这些库大概率都是标准 JS 来编写的。
如上图,这个 3D 人物动画是一个经典的 CPU 计算密集的测试用例,且可以直观感受到性能在帧数上的表现(
http://aws-website-webassemblyskeletalanimation-ffaza.s3-website-us-east-1.amazonaws.com/)。感兴趣的同学可以在自己浏览器里尝试一下,当 3D 人物数量为100时 JS 版本会有明显卡顿,切换到 Wasm 则不会有卡顿感。
这是在 17 年 Wasm 诞生之初的测试,可以看到在不同的环境下 Wasm 比标准 JS 快了 8-15 倍。随着 JS 的不断优化,现在再去测试可能就不会有这么大的差异了。更重要的是,这个测试用例不一定能代表真实的 Web 应用,真正的 Web 应用可能不会命中这么多"优化项",8 倍以上的性能差异往往只存在于测试用例中。这里我必须再重复一下就是,Wasm 快 10% 到 1000% 都有可能,不同的测试环境下不可一概而论。
3.4 如何正确使用 WebAssembly?
首先需要再次强调的是,WebAssembly 的诞生并不是要取代 JavaScript,Web 端整个主框架还是 HTML+JS+CSS 这一套。Web 应用的大部分基础功能也依然是靠 JavaScript 来实现,我们只是将 Web 应用中对性能有较高要求的模块替换为 wasm 实现。在这样的场景下,正确使用 WebAssembly 的步骤为:
1. 整理 Web 应用中所有模块,梳理出有性能瓶颈的地方。例如你的 Web 应用里有视频上传、文件对比、视频编解码、游戏等模块,这些都是很适合用 WebAssembly 来实现的。相反,基础的网页交互功能并不适合用 WebAssembly 来实现。
2. 进行简单的 demo 性能测试,看是否能达到预期的加速效果。如果加速效果并不明显,那么就不适合切换到 Wasm。
3. 确定用来编译成 WebAssembly 的源语言,目前主流的语言基本都是支持 WebAssembly 的,唯一不同的区别是其编译器的优化程度。如果你使用过 C++、RUST,最好还是用这两种语言来编写,其编译优化程度会更高。当然了如果你想使用 PHP/GO/JS/Python 这些你更加熟悉的语言的话,也是不错的选择,毕竟有时候开发效率会比运行效率要更加重要。
4. 编码实现,然后导出 .wasm 文件。这一步基本没什么难度,确定了语言之后使用对应的编译器即可,需要注意的是记得尽量多打开 debug 选项,不然有运行时报错的话你就只能对着一堆二进制代码懵逼了。
5. 编写 JavaScript 胶水代码,加载 .wasm 模块。在最小可行版本的实现中,在 Web 上访问 WebAssembly 的唯一方法是通过显式的 JavaScript API 调用,而在 ES6 标准中,WebAssembly 也可以直接从<script type='module'>的 HTML 标签加载和运行。
3.5 使用示例
3.5.1 快速运行试验
看了刚才运行 WebAssembly 的步骤,是否觉得还是有些繁琐呢?没关系,这里教你一个快速体验运行 WebAssembly 的方法:
1. 打开任意的浏览器,例如 Chrome。
2. 按 F12,启动开发者工具。
3. 找到 Console 页签,复制这一段代码,回车运行。
WebAssembly.compile(newUint8Array(`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`.trim().split(/[\s\r\n]+/g).map(str =>parseInt(str, 16))
)).then(module => {
const instance = new WebAssembly.Instance(module)
const { add, square } = instance.exports
console.log('2 + 4 =', add(2, 4))
console.log('3^2 =', square(3))
console.log('(2 + 5)^2 =', square(add(2 + 5)))
})
这里我们是通过直接手写二进制机器码的方式生成了一段 wasm 代码,并使用了 WebAssembly.compile 接口来进行编译,最后调用了 wasm 实现的 add 和 square 函数。如果顺利的话,你的浏览器会编译这段 WebAssembly 代码 并调用执行,输出对应的计算结果,具体如下图所示:
当然,如果如果没有按预期输出的话,那就说明你当前的浏览器版本是不支持 WebAssembly 的。
WebAssembly 在 Web 端的应用
一家名为"Scott Logic"的软件开发商在2022年6月发布了2022年 WebAssembly 现状报告(这个统计允许开发者选择多个选项,所以总和是大于100%的),在关于 WebAssembly 应用的统计中有几个信息值得关注:
1. 首先 ,WebAssembly 最多的应用场景依然是在 Web 站点开发上,大约占65%。
2. 其次 ,WebAssembly 在 Serverless 和容器化方面的应用大幅增加,由去年的20%提升到了35%。
3. 最后 ,增长幅度最大的是在"作为插件环境"应用场景,WebAssembly 的沙盒化安全环境很适合用于托管不受信任的第三方代码。
本章会介绍一些公司内外的 Web 端应用场景,关于服务端的应用会在第五章继续介绍。
4.1 常见 Web 端应用概览
4.1.1 【Google-可视化】谷歌地球 3D 地图
在最早的版本 Google Earth 是只能跑在 Chorom 浏览器的,因为其底层用的是跟 WebAssembly 类似的 Native Client 技术。目前的 Google Earth 已经可以运行在 Firefox、Edge、Opera 浏览器,其关键的一点就是用WebAssembly 代替了原来 Native Client。
4.1.2 【Bilibili-编解码】哔哩哔哩视频网站
B站 视频上传的功能里有大量的 Wasm 模块,类似视频上传、封面图处理这些都是计算比较密集的场景。如上图所示,B站 用到了 Wasm 版 FFmpeg 来加速视频编解码,这应该是 WebAssembly 最常见的应用了。除此之外还用到了Wasm 版 Tensorflow,这里应该是用来实现 "AI智能生成封面" 的功能。
4.1.3 【Figma-设计工具】Figma 在线UI设计
Figma 是近年来少有的可以称得上拥有「硅谷速度」的创新型公司。2018年初,Figma 的估值才刚刚过1亿美元,还仅仅是一个小众设计工具,到了2021年,Figma 估值暴涨100倍来到了100亿美元,其在设计圈的地位已经足以跟此前几乎处于垄断地位的 Adobe 产品抗衡,成为了产品圈、设计圈内人人必用的工具。
Figma 可以说是典型的 WebAssembly 应用了,使用了 zaplib(一款基于 wasm 和 Rust 的高性能 Web 应用框架)来进行开发。外围的交互操作还是用原生的 JS+CSS+HTML 来实现的,中间核心绘图区域是一个由 wasm+webGL 来驱动的的 canvas 模块。
4.1.4 【Adobe-设计工具】Photoshop Web 版
就在几年前,直接在浏览器中运行像 Photoshop 这样复杂的软件的想法还很难想象。然而,通过使用各种新的网络技术,Adobe 现在已经将 Photoshop 的公开测试版带到了网络上。
Adobe 工程师这里所说的新技术,其中很重要一部分就是 WebAssembly。除了解决性能问题,更重要的是 Photeshop 的 Web 端和 PC 端应用可以由同一份源码编译生成。Adobe 使用 Emscripten 将 Photeshop 的完整 C++ 工程直接移植到了 Web 端,而无需用 JS 重写。Emscripten 是一个功能齐全的工具链,它不仅可以帮你将 C++ 编译为 Wasm,还提供了一个转换层,可以将 POSIX API 调用转换为 Web API 调用,将 OpenGL 转换为 WebGL。
4.1.5【Zoom-在线会议】Zoom Web版
将 Zoom 移植到 Web 端,其复杂程度绝对不低于前面所说的几个应用。除了视频流的处理,Zoom 还提供了自动字幕、虚拟背景等功能,这些都是典型的 CPU 计算密集应用。ZoomWeb 的核心是 WebRTC,在 WebAssembly 诞生后, Zoom 的工程师将 WebAssembly SIMD 的能力引入了 ZoomWeb。WebAssembly SIMD 提供了可移植、高性能的 SIMD 命令集,可用于目前绝大多数主流 CPU 架构。音视频编解码、图像处理这些都是 SIMD 的典型应用场景,ZoomWeb 中虚拟背景的底层计算就是利用 WebAssembly SIMD 来实现的。
4.1.6【Google-机器学习】TensorFlow.js
TensorFlow.js 是一个 JavaScript 库,用于在浏览器和 Node.js 训练和部署机器学习模型。在 2020 年,TensorFlow.js 引入了一个新的 WebAssembly 加速后端。从 TensorFlow.js 2.3.0 版开始,Wasm 后端通过 XNNPACK 利用 SIMD 指令和多线程,速度提高了 10 倍,其中 XNNPACK 是一个高度优化的神经网络运算符库。
TensorFlow.js 从 2.1.0 开始支持 SIMD,从 TensorFlow.js 2.3.0 开始支持多线程。Wasm SIMD 是 wasm 标准第 3 阶段的提案,Wasm threads 是 wasm 标准第2阶段的提案,目前绝大多数浏览器环境都可支持该两种能力。SIMD 和多线程的性能增益彼此独立。TensorFlow 的基准测试表明,SIMD 为普通 Wasm 带来了 1.7-4.5 倍的性能提升,而多线程在此之上又带来了 1.8-2.9 倍的加速。
4.1.7 【FFmpeg-音视频处理】
FFmpeg 就不用多介绍了吧,20多年前 Fabrice Bellard 发起的 FFmpeg 项目不知道养活了多少公司和音视频开发者。XX 播放器,XX 格式工厂基本都是在 FFmpeg 上面套了个 UI。ffmpeg.wasm 的意义就在于可以不再完全依赖浏览器的音视频能力,强大的几乎支持所有格式的音视频处理能力可以被移植到 Web 端。根据目前了解到的信息,FFmpeg 在操作系统、硬件、驱动等环境支持的情况下,是可以利用 GPU 或者其它硬件来加速解码的。大多数浏览器也都支持硬件编解码加速,但运行在浏览器内的 ffmpeg.wasm 应该是只能纯 CPU 软解的,这里可能会存在一定的性能问题。
4.1.8【Unity/Unreal-游戏引擎】H5 游戏开发、Web 端游戏运营工具
这是 Unity 在4年前发布的一个 demo,使用Unity开发并发布到 Web 端。其游戏效果已经很好了,且可在浏览器里流程运行。使用原始 HTML5 技术如果想达到跟这个 demo 一样的体验和性能,投入的成本将会非常大。目前所有版本的 unity 以及 Unreal4.18 之前版本的 UE,都支持将游戏内容打包发布到 Web 端。在 Unity 里的平台名叫"webGL",在 UE 里则是"HTML5"。将游戏内容发布到 Web 端,主要需要解决3个问题,首先是将引擎的底层代码和脚本代码编译成 wasm 的方式来执行,其次引擎的"平台无关层"需要适配 webGL 这个图形 api,最后则是适配浏览器的系统接口。在 wasm 未诞生之前,引擎则是将代码转成 asm.js 来执行。至于 UE 为什么在后面的主线版本不再支持 Web 端,官方给个说法是"未达到预期效果且不好维护"。
WebAssembly 在服务端的应用
看到这里你也许会觉得疑惑,WebAssembly 不是跑在浏览器里的前端技术?为什么能跟服务端的 Docker、K8S、容器化这些概念扯上关系?就像之前文章说到的,这绝对不是一个仅限于前端的新技术,WebAssembly 有着更远大的的宏图大业。
Docker 的创始人 Solomon Hykes 在 2019 年 3 月份发布了一条 Twitter 引起了众多讨论,译文如下:
如果2008年的时候,WASM 和 WASI(WebAssembly System Interface, WASM系统接口)这两个东西已经存在了的话,我们就没有必要创立 Docker 了。在服务器上运行 WebAssembly 是计算的未来,目前缺少的就是一个标准的系统接口,希望 WASI 能够弥补上这块缺失的拼图。
5.1 WASI:解决跨平台运行操作系统的差异
如下图所示:WebAssembly 运行在浏览器内,与系统交互靠的是 JS 胶水语言的能力,JS 通过浏览器内核再到操作系统内核。而 WebAssembly 脱离了浏览器后,运行在各个操作系统中也需要抹平系统 api 的差异性,这就是 WASI 需要解决的问题。
WASI(WebAssembly System Interface, WASM 系统接口),这里的系统接口指的就是例如文件操作、网络连接、系统时钟、随机数之类的操作系统调用,开发 WASI 的唯一目的就是将 WebAssembly 向浏览器之外推进,最终能够真正做到一份 wasm 代码运行在所有不同环境不同操作系统的机器中。
例如 C 这样的语言可以跨平台运行,这实际上是源码级的跨平台,一次编写多次编译,编译器根据目标平台选择对应的系统 api。如下图所示:C 源码被 clang 编译了3次,生成了三份对应不同目标平台的机器码。
wasm 是二进制级别的跨平台,这种可移植性让用户分发代码更容易。wasm 只需要被提前编译一次,就能在不同操作系统上运行。在编译的时候并不确定其目标平台,wasi 这里实际需要的是一个跨平台的 runtime!如下图所示:C 源码只被编译了1次,.wasm 通过 WebAssembly runtime 运行在不同系统中。
看到这是不是有种熟悉的感觉了?因为 Java 也就是这么干的,WebAssembly runtime 对应的就是 JVM,.wasm 则对应 java bytecode。所不同的是, WebAssembly 支持了更多的语言,而且运行在浏览器里支持更加完备。
5.2 WASI现在的进展?
WASI 实际上是一个标准,目前最主流的实现方案是 Bytecode Alliance 使用 Rust 开发的 Wasmtime。截止到我写这篇文章的时候已经有11.3K的 star。看了最新 git 记录,整个开发应该是仍然处于"疯狂打码中"的状态。
在2022年9月,Bytecode Alliance 发布了 Wasmtime1.0:
快!安全!能够用于生产环境!这就是开发团队对1.0版本最直接的介绍,如果说以前 WASI 还处于探索阶段,这个版本的推出已经意味着 WASI 可以在生产环境进行更多的尝试了,整个社区目前也是非常的活跃。
5.3 WebAssembly 在服务端的应用
在云计算的概念里,服务端的容器虚拟化大概可以划分为三个不同的抽象层:
1. Hypervisor VM,或者又称 microVMs,其是最底层的虚拟方案,能够直接与硬件进行交互。常见案例有:AWS Firecracker、VMware。
2. 在往上一层是 Application containers,所熟知的 Docker 就在这一层,依然是比较"重"的虚拟方案。
3. 最上层是 High level language VMs,JVM、Python runtimes 以及 WebAssembly 都属于这一层。
那么在服务端,WebAssembly 到底可以应用在哪些方面?其优势是什么呢?官方给出的建议是有以下五个场景是比较适合的:
一、微服务/无服务平台 ,WebAssembly 是非常适合用作微服务和无服务平台的。后端即服务(Backend as a Service,BaaS),函数即服务(Function as a Service,FaaS)都可以归属到 severless 无服务模型。WebAssembly 的启动时间相比 Docker 或者其它 VM 要快很多,WebAssembly 的运行时是非常"轻"的, 启动一个 WebAssembly 实例只需要5微秒 。除此之外,轻量级所带来的另外一个优势就是 可以在一台机器上搭载更多实例 。
二、第三方插件系统 ,当平台需要运行第三方开发者的代码,安全性就是不可避免的问题。而 WebAssembly 是沙盒化的 ,并且第三方程序无法访问未明确交给给它的任何系统资源。除此之外,平台和第三方插件之间的通信也是很快的。
三、为数据库实现 UDF 功能 ,UserDefineFunction(用户自定义函数,UDF)是数据库应用程序加速的一种方法。指的是将逻辑代码放到数据库中运行,通过降低应用程序和数据库之间的交互开销来提升整个程序的运行效率。例如 Google BigQuery 允许用户从 SQL 查询调用以 JavaScript 编写的代码,阿里云的 MaxCompute 可以直接将 Java 或 Python 代码作为 UDF 嵌入 SQL。数据库可以基于 WebAssembly runtime 来实现 UDF 能力,其优势在于: 支持更多语言、安全隔离、跨平台、性能好、冷启动快等。
四、搭建可信执行环境 , Trusted execution environments (TEEs)指的是为不想或者不能信任底层系统的应用程序单独开辟一个在 CPU 上安全运行环境,此时的 TEE 应用程序独立于其它操作系统、虚拟环境、内核以及其它系统软件。TEE技术常用于移动支付、隐私计算等安全性要求较高的场景。使用WebAssembly搭建 TEE 的优势在于: 支持更多语言、WebAssembly 运行时支持大多数主流 CPU 架构。
五、开发可移植的应用程序, 借助跨平台的 WebAssembly runtime,WebAssembly 应用程序可以运行在不同 CPU 架构、不同操作系统的计算机上。开发者只需要专注于程序的逻辑功能,而不需要过多担心平台的差异性、性能、安全等问题。
总结
第一个问题:你的 Web 应用性能瓶颈在哪里? 先想清楚这个问题再做优化本文的主角并非 webGL,但是文章里多次不可避免的提到,其根本原因就在于 Wasm 解决的是 Web 端 CPU 计算密集的性能问题,而性能瓶颈可能压根就不在这里。Figma 就是最典型的例子,他们使用wasm将应用移植到 Web 端,并对 Web 端的性能进行了大量优化,但最后复盘发现性能提升的真正来源其实是 webGL 渲染器的改进,也就是 GPU 硬件加速的收益,显然这跟 WebAssembly 并没有任何关系。
在之前我们有提到,Unreal 在4.23版本之后将 Web 端的支持从主线分支移除。但近期有一家叫做"Wonder Interactive"创业公司又将这部分能力弥补了回来,并且将在5月份的洛杉矶 GamesBeat 进行宣讲。在他们计划的工作里,UE5 的 Web 端支持将对接最新的 WebGPU 来实现,游戏 AI、寻路等场景也可以用 WebGPU 新增的通用计算(GPGPU)接口来加速。除此之外,游戏资源的压缩、下载和加载也都需要被考虑,WebAssembly 提供的能力也只是整个流程中的一个环节。
第二个问题:现在已经是2024年了,WebAssembly 到底算成功了吗? 如果要从技术的成熟度上来说,我认为是成功的,WebAssembly 已经投入到了大量的生产应用中。
如果要从推广应用的角度来说,我认为目前是不成功的,因为90%以上的场景其实不需要 WebAssembly。
另外一个角度来说就是, WebAssembly 很快,但是还不够快,不足以让开发者完全转向拥抱一个崭新的技术。