边学边译JS工作机制--6.WebAssembly以及使用它的场景

737 阅读8分钟

本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)
本文推荐指数: 2
本文属于科普性质,阅读一下涨涨姿势就好

这是JS工作机制的第6章

这一次我们将来研究一下WebAssembly的工作原理,分析一下在为啥在这些指标上比JS表现更好:加载事件,执行速度,垃圾回收,内容使用,平台API,调试以及多线程和可移植性。 构建Web应用的方式正在革命的边缘---虽然这只是早期,但是确实要思考一下构建方式将要改变了!

WebAssembly 是啥

WebAssembly (以下简称 wasm) 是一个高性能的, 给Web使用的底层字节码。

WASM 让你使用更多的编程语言来写程序(比如 C, C++, Rust 及其他), 然后编译成WebAssembly。 这样web应用可以快速加载和执行。

加载时间

浏览器需要加载所有的.js文本文件,这样才能记载JavaScript。

浏览器加载WebAssembly更快,因为只要传递已经表编译好的wasm文件。wasm是底层的 assembly类语言,只是一个非常紧凑的二进制格式。

执行

运行Wasm只比本地码执行慢20%,这个是amazing的结果。这种格式,是在一个沙箱环境中编译的,运行时也加了很多的限制来确保没有安全漏洞,或者强化它们。最小化的本地代码相比,它是慢了一点,但是在未来肯定会更快的。

更棒的是,它是浏览器无关的---也就是大多数的浏览器引擎已经支持了WebAssembly,并且提供了非常相近的执行时间。

看一下V8中发生了什么:

image.png

在左边,我们有一些JS资源,包含了JS函数。首先需要转换,把所有的字符串转换成AST。这个AST是你的JS程序的逻辑的内存描述。一旦AST生成了,V8 直接转换成机器码。遍历树,然后生成机器码,在这里你获得了你编译好的函数。这里没有尝试去加速。

下一步,V8 的管道将会发生什么呢?

image.png

这次我们有[TurboFan],这是V8中的一个优化过的编译器。当你的JS 应用在运行,代码运行在V8中。TurboFan监听是否有东西运行慢了,是否是瓶颈了,就将其标记为热点,以便于优化他们。它在后台推送他们,这个后台是优化过的JIT,JIT为这些函数创建了更快的代码,但是要吃掉大部分的CPU。

它解决了问题,但是这些分析和优化的流程耗费CPU资源。在移动设备上,就意味着更高的电池消耗。 但是,wasm 不需要这些了--它像这样插入工作流程:

image.png

wasm已经在编译阶段优化了。因此,转化也不需要了。你直接得道了一个优化过的二进制代码,直接注入到后端,生成机器码。所有的额优化已经被前面的编译器做完了。

这样让wasm的执行更加的高效,因为只有少数的步骤要处理,可以略过。

内存模型

image.png

假如C++程序编译成了WebAssembly,这个程序的内存模型是连续的内存块,没有'洞'。wasm的一个有助于安全启动的特性是,执行栈的概念从线性内存中独立了。在C++程序中,你有堆,可以从堆得顶部分配内存,然后添加到栈空间。获取一个指针,然后在栈内存中查找去操作本不应被你接触的变量值。

这给恶意软件提供了一个漏洞。

使用了完全不同的方式。 执行栈从WebAssembly程序中剥离出来了,所以没有办法改变内部的东西比如像变量。 同样,函数在内存中使用整数偏移量而不是指针。函数指针存放在一个中间的函数表中。他们直接计算了模块方法中的跳转量。这种构建方式,你可以并行加载多个wasm模块,偏移所有的索引并且可以正常工作。

如果想知道更多关于内存模型和管理的细节,请查看我们的第四章内容。

GC

已知JS的内存管理是由GC处理的。 WebAssembly有点不同。它支持的语言是手动管理内存,在你的wasm谋爱中你可以主宰自己的GC,但是这是一个复杂的工作 如今,WebAssembly是被设计围绕C++ 和 RUST的。因为wasm是很底层的,使用仅在它之上一层的编程语言来编译更加容易。C语言可以使用malloc,C++ 可以使用智能指针,rust使用一个完全不同的方式(既控制权转移等)。这些语言不需要GC,所以他们不需要任何复杂的运行时去跟踪栈内存。WebAssembly天然适配这些语言。

这些语言不是100% 的设计出来去处理复杂的JS事务,像DOM操作。用C++写一整个HTML应用是不合理的,因为不是设计给它的。在大多数情况下,工程师写C++,rust 目标是WebGL或者高度优化的库(比如一些重数学计算的) 未来,WebAssembly将会支持更多不带GC的语言

平台 API

依赖于执行JS的运行时,可以通过 JavaScript 程序来直接访问这些平台所暴露出的指定接口。例如,你在浏览器中运行JS,你需要一个 Web APIs 的集合。这样你的应用可以控制浏览器/设备的功能以及 DOMCSSOMWebGLIndexedDBWeb Audio API,等

WebAssembly模块没有权限访问任何平台的API,一切都要靠JS来中介。如果你想访问一些平台特有的API,你需要通过JS来调用。

例如,如果你想调用console.log,你需要通过JS而不是C++,这样的话自然会有一些性能损耗。
但这不会是常态,规范将会提供更多的平台api,这样你就不用写JS了

Source maps

当你的JS代码已经最小化了,你需要一个办法去正确的调试代码。这就是SourceMap出现的地方。

SourceMap是映射合并/最小化的文件到未构建状态的一种方式。只要构建产品,就要生成一个SourceMap,它包含了原始文件的所有信息。当你在生成的JS中查询一个确定的行或者列的数字,你可以查找SourceMap,它会返回一个原始的位置。

WebAssembly 当前不支持SourceMap,因为规范没写,但是它将来会支持的(或许很快)

当你在C++代码中打一个断点在,你将会看到C++代码而不是WebAssembly。这是我们的目标。

多线程

JS是单线程的。虽然JS使用 Web Workers,但是他们有一些指定的场景----基本上,任何CPU密集型计算都会阻塞你的主UI线程,这时候使用 Web Workers是有好处的。但是, Web Workers不能访问DOM WebAssembly当前也不支持多线程。但是,它即将到来了。Wasm将很接近本地线程(像C++ 风格的线程)。具有真线程将会创建很多新的机会。当然,也会打开滥用之门。

可移植性

如今JS可以在任何地方运行,从浏览器到服务端,甚至在嵌入式系统。

WebAssembly设计来安全和便携的。就像JS。它会运行在任何支持wasm的环境中。(比如每一个浏览器)

WebAssembly 具有同样的可移植性目标,就像JAVA早期使用Applets所尝试的那样。

哪些场景下使用WebAssembly比JS好?

在WebAssembly的第一个版本中,主要聚集于重CPU运算的场景。最主流的是游戏---他们有海量的纹理计算。你可用C++/rust写程序,绑定到你熟悉的OpenGL 上,然后编译成wasm。然后在浏览器中运行。

另一个使用WebAssembly(高性能)场景是实现一些库,这些库承担了很重的CPU密集型工作。例如,图像操作

前面提到,wasm可以减少一些移动设备的电池损耗,因为大多数处理步骤在编译前就已经被提前做好了。

未来,你可以直接使用 WASM 二进制库即使你没有编写编译成它的代码。你可以在 NPM 上面找到一些开始使用这项技术的项目。

针对操作 DOM 和频繁使用平台接口的情况 ,使用 JavaScript 会更加合理,因为它不会产生额外的性能开销且它原生支持各种接口。