前言
一直以来,JavaScript作为一门解释型语言,其在密集计算的场景下,和C++等语言相比,相去甚远。同时随着音视频会议、3D 游戏、虚拟/增强现实以及图像/视频编辑等重量级的应用开始搬迁到 Web 前端, JavaScript的性能短板开始凸显,一种可以在Web端进行高性能密集计算的方案成为了一个很迫切的诉求。而WebAssembly就是在这样的情况下应运而生,它可是使各种语言编写的代码都可以以接近原生的速度在 Web 中运行。从而使以前无法以此方式运行的客户端软件都将可以运行在 Web 中。下面我们一起去了解学习一下WebAssembly 。
一、WebAssembly是什么?
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
WebAssembly 是基于栈式虚拟机的二进制指令集,可以作为编程语言的编译目标,能够部署在 web 客户端和服务端的应用中。
听起来有点晦涩,下面说说我个人的一些理解。
WebAssembly(Wasm)是Web中的指令集,或者说其目的是让指令集能够在Web中运行。这其中又包含两个层面:
Assembly的来源
Wasm作为媒介(编译目标),它可以:
抹平语言差异
抹平终端差异
目前已经有了将C/C++、Rust、Ts、C#、Go、Kotlin、Swift等语言转换为WebAssembly(Wasm) 的工具,后续应该会越来越多。
Wasm的运行环境
我们需要运行时环境去解析执行转换后的指令集。这个运行环境并不仅仅包含浏览器。下面是来自WebAssembly官网的一张环境以及特性支持状况的截图
从图中我们可以看到:
V8层面:目前Chrome,Firefox,Safari三大浏览器都对Wasm有不错的支持,而且特性的支持发展的很快。
除了浏览器,还有一些非浏览器的环境。比如Wasmtime,Wasmer,Nodejs等等也都快速发展而且竞争非常的激烈。
二、WebAssembly为什么快?
Wasm的目标就是为了解决Web端密集计算场景下的问题。那么它为什么能解决?为什么会有更快的性能呢?在解释这个问题之前,我们首先需要理解 JS 引擎所做的工作。如下图:
Parsing - 将源码转换成解释器可以运行的东西所用的事情。
Compiling + optimizing - 花费在基础编译和优化编译上的时间。有一些优化编译的工作不在主线程,所以这里并不包括这些时间。
Re-optimizing - 当预先编译优化的代码不能被优化的情况下,JIT 将这些代码重新优化,如果不能重新优化那么就丢给基础编译去做。这个过程叫做重新优化。
Execution - 执行代码的过程
Garbage collection - 清理内存的时间
这是JS引擎运行的5个步骤,下面我们再来看看Wasm的加载过程:
两相比较我们可以发现:
解析
JavaScript 源码一旦被下载到浏览器,源码将被解析为抽象语法树(AST)。通常浏览器解析源码是懒惰的,浏览器首先会解析他们真正需要的东西,没有及时被调用的函数只会被创建成存根。在这个过程中,AST被转换为该 JS 引擎的中间表示(称为字节码)。
相反,WebAssembly 不需要被转换,因为它已经是字节码了。它仅仅需要被解码并确定没有任何错误。
编译 + 优化
如前所述,JavaScript 是在执行代码期间编译的。因为 JavaScript 是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译。这样会消耗时间。
相反,WebAssembly 与机器代码更接近。例如,类型是程序的一部分。这是速度更快的一个原因:
编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化。
编译器不需要去检查多次执行相同代码中数据类型是否一样。
更多的优化在 LLVM 最前面就已经完成了。所以编译和优化的工作很少。
重新优化
有时 JIT 抛出一个优化版本的代码,然后重新优化。
JIT 基于运行代码的假设不正确时,会发生这种情况。例如,当进入循环的变量与先前的迭代不同时,或者在原型链中插入新函数时,会发生重新优化。
在 WebAssembly 中,类型是明确的,因此 JIT 不需要根据运行时收集的数据对类型进行假设。这意味着它不必经过重新优化的周期。
执行
执行 WebAssembly 代码通常更快。有些必须对 JavaScript 做的优化不需要用在 WebAssembly 上
另外,WebAssembly 是为编译器设计的。意思是,它是专门给编译器来阅读,并不是当作编程语言让程序员去写的。
由于程序员不需要直接编程,WebAssembly 提供了一组更适合机器的指令。根据您的代码所做的工作,这些指令的运行速度可以在10%到800%之间。
垃圾回收
在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。
这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。
现在,WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像 C/C++)。虽然这些可能让开发者编程更困难,但它的确提升了性能。
综上所述,WebAssembly代码在优化和执行上都有更高的效率。同时也节省了Parsing,重新优化和GC。所以性能上有了质的提升。
那么开发一个WebAssembly容易么?下面我们通过一个Demo体验一下。
三、WebAssembly初体验
目前Wasm的开发已经有了一定的生态,可以帮助我们大大节省Wasm的配置时间,提高开发效率。下面我们通过一个小Demo来体验一下Wasm的开发。*Rust和wasm-pack的安装请自行搜索,本文不再赘述。
第一步:我们先写一段Rust代码
extern crate wasm_bindgen;
这段代码的含义是:Wasm提供一个notify方法,可以供js调用。而notify又会调用Web的alert去弹出传入的参数。
第二步:编译代码
wasm-pack build --target web
编译后我们可以看到,wasm-pack直接帮我们生成了很多的内容,包括wasm文件,ts声明文件,module引入js文件以及module引入ts文件
第三步:创建HTML
<html>
<head>
<script type="module">
import init from './cdtest2.js';
init().then(js => {
js.notify(12345);
});
</script>
</head>
</html>
第四步:浏览器运行
通过这个小例子,我们可以看到,开发一个Wasm在部署和生成方面还是蛮简单的。但在实际项目中,并不会这么理想,目前还存在不少问题,比如文件体积过大。上面这个小小Demo生成的Wasm文件大小竟然有17K。那当我们遇到这种编译问题时怎么办呢?
四、WebAssembly文本格式
WebAssembly会被编译成扩展名为.wasm的二进制文件。为了能够让人阅读和编辑 WebAssembly,Wasm提供了一种基于 S-表达式的文本表示形式。这是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。让我们看一个简单的例子——下面的程序从一个叫做 imports 的模块中导入了一个叫做 imported_func 的函数并且导出了一个叫做 exported_func 的函数:
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i
)
)
WebAssembly 函数 exported_func 是被导出供我们的环境(比如,使用了 WebAssembly 模块的网络应用)使用。当被调用的时,它进而调用了一个被导入的叫做 imported_func 的函数并且向该函数传递了一个值(42)作为参数。比如上面的例子,我们生成了一个Wasm文件:cdtest2_bg.wasm,我们可以通过工具wasm2wat,把它转换成wat文件也就是文本格式。
文本格式的意义:
- 可以不依赖其他语言,直接编写Wasm程序
- 可以让我们把Wasm转换成Wat,以方便我们阅读,分析排查问题
- 我们可以把文本格式当成IL,来构建我们自己的编译器
所以当我们遇到编译问题的时候,有能力的话可以去自己生成文本格式然后转成Wasm。
其实笔者开始对Wasm,在前端安全方面是抱有期待的。比如把秘钥包含在二进制文件中,让其具备更好的私密性。但通过上述我们知道,二进制可以轻松转换成文本格式,所以期待落空了。之前我们也说了,运行时不仅仅是浏览器。
五、起始于Web但又超出Web
通过WASI(WebAssemblySystemInterface,Wasm操作系统接口)标准,Wasm可以直接与操作系统打交道。通过已经在各种环境实现了 WASI 标准的虚拟机,我们就可以将 Wasm 用在嵌入式、IOT 物联网以及甚至云,AI 和区块链等特殊的领域和场景中。Wasm 在 Web 浏览器之外, 尤其是在服务器端和云原生环境中。具备四项优势:
可移植性: 从公有云向边缘设备望去, 硬件与系统愈发多样; Wasm 在各种平台实现了个遍, 且更能支持资源有限的嵌入式环境
快速: 基准测试显示, Wasm 速度接近或达到原生应用
小巧: Wasm 应用的镜像大小通常比 docker 镜像小 1~2 个数量级, 而且可流式加载, 实例化速度甚至可达到微秒级
安全: Wasm 有一个现代的安全模型, 其设计体现了零信任的思想
其实对于非浏览器环境,只要能提供一个符合WASI标准的Runtime,那么就能够运行标准的Wasm指令集。后续WebAssembly的应用场景会越来越宽泛,非常值得期待。
总结
其实关于WebAssembly,可深入研究的东西还有很多。本文主要还是基于科普,介绍一些相关的基本概念,以及前端比较关注的点。同时随着WebAssembly的兴起,势必会改变前端的局部开发形态。同时我们也会积极在公司内部,寻找WebAssembly可落地实践的点,去优化提升我们的程序性能。