浅谈WebAssembly
1、WebAssembly是什么?
WebAssembly
是一种低级的类汇编语言,具有紧凑的
二进制格式
,可以接近原生的性能运行。它设计的目的不是为了手写代码而是为诸如C、C++、Rust、AssemblyScript等低级源语言提供一个高效的编译目标。
2、WebAssembly有什么意义?
和 JS 需要解释执行不同的是,WebAssembly 字节码和底层机器码很相似可快速装载运行,因此性能相对于 JS 解释执行大大提升,为客户端app提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式。解决
3D游戏、虚拟现实、增强现实、计算机视觉、图像/视频编辑
在浏览器上的性能问题。
WebAssembly的模块可以被导入的到一个网络app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函数。性能更快、更快、更快!
3、为什么WebAssembly性能更快?
想要知道JS 引擎运行程序花费的时间。我们需要知道JavaScript 做了什么事情。有几个阶段:
Parsing
- 源码转换成解释器可以运行的东西所用的时间。
Compiling + optimizing
- 花费在基础编译和优化编译上的时间。
Re-optimizing
- 当预先编译优化的代码不能被优化的情况下,JIT 将这些代码重新优化,如果不能重新优化那么久丢给基础编译去做。这个过程叫做重新优化。
Execution
- 执行代码的过程
Garbage collection
- 清理内存的时间
需要注意的是:这些任务不会发生在离散块或特定的序列中。相反,它们将被交叉执行。比如正在做一些代码解析时,还执行者一些其他的逻辑,有些代码编译完成后,引擎又做了一些解析,然后又执行了一些逻辑,等等。
3.1 JavaScript和WebAssembly性能比较
3.1.1 请求
文件小
:从服务器获取文件是需要时间的,下载执行与 JavaScript 等效的 WebAssembly 文件需要更少的时间,因为它的体积更小。WebAssembly 设计的体积更小,是一种二进制形式。即使使用 gzip 压缩的 JavaScript文件很小,但 WebAssembly 中的等效代码可能更小。下载资源的时间会更少。
3.1.2 解析
字节码
:JavaScript 源码一旦被下载到浏览器,源将被解析为抽象语法树(AST)。在这个过程中,AST需要被转换为该 JS 引擎所能识别的字节码。
相反,WebAssembly不需要被转换,因为它已经是字节码了。它仅仅需要被解码并确定没有任何错误。
3.1.3 编译 + 优化
JavaScript 是在执行代码期间编译的。因为 JavaScript 是动态类型语言,相同的代码在多次执行中都有可能都因为代码里含有不同的类型数据被重新编译。这样会消耗时间。
相反,WebAssembly与机器代码更接近。编译器不需要在运行代码时花费时间去观察代码中的数据类型,在开始编译时做优化。编译器不需要去判断每次执行相同代码中数据类型是否一样。
3.1.4 重新优化
无需重新优化
:在 WebAssembly中,类型是明确的,不需要根据运行时收集的数据对类型进行假设。这意味着它不必经过重新优化的周期。
3.1.5 执行
WebAssembly 提供了一组更适合机器的指令、执行速度更快。
3.1.6 垃圾回收
在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。
这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。
现在,WebAssembly根本不支持垃圾回收。内存是手动管理的(就像 C/C++)。虽然这些可能让开发者编程更困难,但它的确提升了性能。
总而言之,这些都是在许多情况下,在执行相同任务时WebAssembly 将胜过 JavaScript 的原因。
3.1.7 兼容性问题
WebAssembly 是非常底层的字节码规范,制订好后很少变动,就算以后发生变化,也只需在从高级语言编译成字节码过程中做兼容。可能出现兼容性问题的地方在于 JS 和 WebAssembly 桥接的 JS 接口。
4、关键概念
有几个关键概念需要注意的是:
模块:表示一个已经被浏览器编译为可执行机器码的WebAssembly二进制代码。主要的特点是:二进制对象(Blob)、无状态、缓存(indexDB)、可以导入导出(但是目前还不支持ES6和script)。
内存:ArrayBuffer、本质上是连续的字节数组。
实例:一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的ES2015模块。
5、具体如何实践?
需要借助Emscripten编译器(自行百度安装),这个编译器能够将一段C/C++代码,编译出:
一个.wasm模块。
用来加载和运行该模块的JavaScript“胶水”代码。
一个用来展示代码运行结果的HTML文档。
简而言之,工作流程如下所示:
Emscripten首先把C/C++提供给clang+LLVM——一个成熟的开源C/C++编译器工具链,比如,在OSX上是XCode的一部分。
Emscripten将clang+LLVM编译的结果转换为一个.wasm二进制文件。
就自身而言,WebAssembly当前不能直接的存取DOM;它只能调用JavaScript,并且只能传入整形和浮点型的原始数据类型作为参数。这就是说,为了使用任何Web API,WebAssembly需要调用到JavaScript,然后由JavaScript调用Web API。因此,Emscripten创建了HTML和JavaScript胶水代码以便完成这些功能。
5.1 JS调用WebAssembly
5.1.1 编译
举个例子:
有hello.c的这么一个文件。
#include int main(int argc, char ** argv) { printf("Hello World\n");
终端窗口中,进入刚刚保存
hello.c
文件的文件夹中,然后运行下列命令:
emcc hello.c -s WASM=1 -o hello.html
下面列出了我们命令中选项的细节:
-s WASM=1
—指定我们想要的
wasm
输出形式。如果我们不指定这个选项,Emscripten默认将只会生成asm.js。
-o hello.html
—指定这个选项将会生成HTML页面来运行我们的代码,并且会生成wasm模块,以及编译和实例化wasm模块所需要的“胶水”js代码。这个时候在您的源码文件夹应该有下列文件:
hello.wasm
二进制的wasm模块代码
hello.js
一个包含了用来在原生C函数和JavaScript/wasm之间转换的胶水代码的JavaScript文件
hello.html
一个用来加载,编译,实例化你的wasm代码并且将它输出在浏览器显示上的一个HTML文件。
注意
:默认情况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。你需要导入
emscripten.h
库来使用
EMSCRIPTEN_KEEPALIVE
宏,在一个函数名之前添加
EMSCRIPTEN_KEEPALIVE
来使用这个宏。
5.1.2加载和运行WebAssembly代码
首先需要把模块放入内存。比如,通过
XMLHttpRequest
或
Fetch
,模块将会被初始化为带类型数组。
WebAssembly还没有和
<script type='module'>
或ES6的
import
语句集成,也就是说,当前还没有内置的方式让浏览器为你获取模块。当前唯一的方式就是创建一个包含你的WebAssembly模块二进制代码的
ArrayBuffer
并且使
WebAssembly.instantiate()
编译它。
Fetch
获取
WebAssembly
模块:
该函数返回一个可以解析为
Response
对象的promise。
我们可以使用
arrayBuffer()
函数把响应(response)转换为带类型数组,该函数返回一个可以解析为带类型数组的promise。
最后,我们使用
WebAssembly.instantiate()
函数一步实现编译和实例化带类型数组。
代码如下:
fetch('module.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
//Do something with the compiled results!
5.2 C/C++调用JavaScript
Emscripten 允许 C / C++ 代码直接调用JavaScript。
新建一个文件hello.c,写入下面的代码。
#include
int main() {
EM_ASM({ alert('Hello World!'); });
EM_ASM
是一个宏,会调用嵌入的JavaScript 代码。注意,JavaScript 代码要写在大括号里面。
然后,将这个程序编译
$ emcc hello.c -o hello.html
浏览器打开hello.html
,就会跳出对话框Hello World!。
5.3 C/C++ 与 JavaScript 的通信
Emscripten 允许 C / C++ 代码与JavaScript 通信。
新建一个文件hello.c
,写入下面的代码。
#include
int main() {
int val1 = 21;
int val2 = 2
int val = EM_ASM_INT({ return $0 * $1; }, val1, val2);
std::cout << "val == " << val<
上面代码中,EM_ASM_INT表示 JavaScript 代码返回的是一个整数,它的参数里面的$0
表示第一个参数,$1
表示第二个参数,以此类推。EM_ASM_INT的其他参数会按照顺序,传入 JavaScript 表达式。
然后,将这个程序进行编译
$ emcc example2.cc -o hello.html
浏览器打开网页hello.html,会显示val2 == 42。
6、编写 WebAssembly
上面介绍的是将C、C++编译成WebAssembly,但是对于习惯写Javascript的前端开发工程师来说成本相对比较大,因此以上的用法适合于将已有的客户端迁移到Web,对于前端开发来说,可以采用AssemblyScript
。
6.1 为什么选 AssemblyScript
作为 WebAssembly 开发语言
学习成本:AssemblyScript
的语法和 TypeScript
一致。AssemblyScript 相对于 C、Rust 等其它语言去写 WebAssembly 而言,学习成本低。
兼容性:对于不支持 WebAssembly 的浏览器,可以通过 TypeScript 编译器编译成可正常执行的 JS 代码,从而实现从 JS 到 WebAssembly 的平滑迁移。
支持webpack构建工具:任何新的 Web 开发技术都少不了构建流程,webpack支持对 AssemblyScript
的构建
6.2 AssemblyScript 用法
举个🌰、 用 TypeScript 实现斐波那契序列计算的模块 f.ts 如下:
export function f(x: i32):i32{
if(x===1 || x===2){
return 1
return f(x-1)+f(x-2)
终端执行:asc f.ts -o f.wasm
。
就能把以上代码编译成可运行的 WebAssembly 模块。
为了加载并执行编译出的 f.wasm 模块,需要通过 JS 去加载并调用模块上的 f 函数,为此需要以下 JS 代码:
fetch('f.wasm') // 网络加载 f.wasm 文件
.then(res => res.arrayBuffer()) // 转成 ArrayBuffer
.then(WebAssembly.instantiate) // 编译为当前 CPU 架构的机器码 + 实例化
.then(mod => { // 调用模块实例上的 f 函数计算
console.log(mod.instance.f(50));
以上代码中出现了一个新的内置类型 i32,这是 AssemblyScript 在 TypeScript 的基础上内置的类型。 AssemblyScript 和 TypeScript 有细微区别,AssemblyScript 是 TypeScript 的子集
,为了方便编译成 WebAssembly 在 TypeScript 的基础上加了更严格的类型限制, 主要区别如下:
比 TypeScript 多了很多更细致的内置类型,以优化性能和内存占用;
不能使用 any 和 undefined 类型,以及枚举类型;
可空类型的变量必须是引用类型,而不能是基本数据类型如 string、number、boolean;
函数中的可选参数必须提供默认值,函数必须有返回类型,无返回值的函数返回类型需要是 void;
不能使用 JS 环境中的内置函数,只能使用 AssemblyScript 提供的内置函数
AssemblyScript 的实现原理本质上是通过 TypeScript 编译器把 TS 源码解析成 AST,再把 AST 翻译成 IR,再通过 LLVM 编译成 WebAssembly 字节码实现的。
6.3 接入Webpack构建
任何新的 Web 开发技术都少不了构建流程,为了提供一套流畅的 WebAssembly 开发流程,接下来介绍接入 Webpack 具体步骤。
安装以下依赖,以便让 TS 源码被 AssemblyScript 编译成 WebAssembly。
"devDependencies": {
"assemblyscript": "github:AssemblyScript/assemblyscript",
"assemblyscript-typescript-loader": "^1.3.2",
"typescript": "^2.8.1",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.10.1"
修改 webpack.config.js,加入 loader:
module.exports = {
module: {
rules: [
test: /\.ts$/,
loader: 'assemblyscript-typescript-loader',
options: {
sourceMap: true,
3.修改 TypeScript 编译器配置 tsconfig.json,以便让 TypeScript 编译器能支持 AssemblyScript 中引入的内置类型和函数。
"extends": "../../node_modules/assemblyscript/std/portable.json",
"include": [
"./**/*.ts"
配置直接继承自 assemblyscript 内置的配置文件。