WebAssembly--执行与内存模型

前言

在WebAssembly初探中,我们已经了解了WebAssembly的发展和标准演进过程,并简单地体验了一把Wasm的应用,本篇文章会通过对比WASM和JS的执行流程,WebAssembly的内存模型深入分析,带大家理解下WebAssembly部分核心原理,探究下其快的秘密。

下图是一组基准测试,可以看出WASM至少比JS运行时性能提升近1倍以上,那为什么WASM快呢,请往下看:

WebAssembly--执行与内存模型_第1张图片

WebAssembly与Javascript

WebAssembly--执行与内存模型_第2张图片

JavaScript 既是解释语言又是编译语言,所以 JavaScript 引擎在解析后启动执行。javascript引擎首先会先将资源下载下来,Javascript 引擎会等待整个资源全部下载完成。一旦资源下载完成,javascript 引擎会对资源进行解析(parse),parse 会将源代码转换成 javascript 解析器能运行的字节码。

JavaScript 引擎的监视器 (在某些浏览器中称为分析器)会跟踪代码执行情况,如果一个特定的代码块被频繁地执行,那么监视器将其标记为热代码。引擎使用即时 (JIT) 编译器编译代码块(比如v8 引擎的 baseline compiler)。整个编译过程发生在主线程,会占用主线程的资源,但被JIT优化后的代码会执行的效率更高,综合的ROI是可接受的,被编译的代码就是优化过程了。

JavaScript 引擎增加了一(或两)层优化,当一个函数调用频率更高时,编译器会标记这个函数并尝试更深度的来优化它。经过这次重新优化 (reoptimisation),编译器会进行更多的推断和生成优化代码。这个优化会花费更多的时间,但是它生成的代码会更快。JavaScript 是一种动态类型的语言,引擎所能做的所有优化都是基于类型的推断。如果推断失败,那么将重新解释并执行代码,并删除优化过的代码,而不是抛出运行时异常,如果重新推断发现类型变化了,那之前的优化就是纯浪费了。

最后一步是垃圾回收,将删除内存中的所有活动对象,JavaScript 引擎中的垃圾回收采用标记清除算法,在垃圾回收过程中,JavaScript 引擎从根对象 (类似于 Node.js 中的全局对象) 开始查找从根对象引用的所有对象,并将它们标记为可访问对象,它将剩余的对象标记为不可访问的对象,最后清除不可访问的对象。

WebAssembly--执行与内存模型_第3张图片

WASM 是二进制格式文件,并且已经被提前编译和优化过成最终的.wasm。在运行过程, JS 引擎会先去加载 WASM 代码,然后解码并转换成模块的内部表达(即 AST)。这个阶段是解码阶段,解码阶段要远远比 JS 的编译阶段要快。

接下来,解码后的 WASM 进入编译阶段,在这个阶段,对模块进行验证,在验证过程中对函数、指令序列和堆栈的使用进行类型检查,然后将验证过的代码编译为机器码。由于 WASM 二进制代码已经提前编译和优化过了,所以在其编译阶段会更快。为了追求提升 WebAssembly 执行的速度,浏览器厂商实现了流式编译机制。流式编译允许 Javascript 引擎一边在 WebAssembly 模块还在下载时,一边进行编译和优化操作。相比于 Javascript 引擎需要等待 Javascript 文件全部下载完成,流式编译提升了这整个过程。

最后编译过的代码进入执行阶段,执行阶段,模块会被实例化并执行。在实例化的时候,JS 引擎会实例化状态和执行栈,最后再执行模块。

WASM 快的原因是因为它的执行步骤要比 JS 的执行步骤少,其二进制代码已经经过了优化和编译,并且可以进行流式编译。但总的来说,WASM 的执行效率并不是一直优于原生JS 代码执行,因为 WASM 代码和 JS 引擎交互和实例化也是要耗费时间的,一些和JS的胶水代码会抵消WASM本身的性能提升,所以需要考虑好使用场景。

WebAssembly内存模型

WebAssembly 模块的内存部分(memory section)是线性内存。

线性内存模型(linear memory model)是一个内存寻址技术,其中内存组织在单个地址空间中(organized in a single contagious address space),也被称为平面内存模型(Flat memory model)

线性内存模型使理解、编程和表示内存变得更容易。但是它也有巨大的缺点,例如重新排列内存中的元素需要大量的执行时间,并且会浪费大量的内存区域。

WebAssembly--执行与内存模型_第4张图片

LLVM 编译 WebAssembly 的时候,会按照固定的内存布局。首先一开始是个数据(Data)区,主要是存放源码的全局数据和静态数据。Wasm 代码里面访问这些变量的时候,是通过使用静态的偏移量调用 iload、istore 来实现的。中间的 Aux Stack 是 Wasm 程序运行中做辅助栈使用的,它与数据区的边界是有一个 Wasm Global 来指向的,叫 data_end,data_end 是个 Global变量。Wasm 程序调用 malloc 时从其自己的 Heap 里面分配数据,heap 区的起始位置是在 heap_base 的 Global 来指定的,它的初始值是编译器在编译时确定的。

WebAssembly--执行与内存模型_第5张图片

线性内存之外的内存区域被称作受管内存 (Managed Memory),这些对象的内存地址有些会逃逸用户的控制。第一种是 Globals,可以把它看成一个一维的数组,这里 data_end 是索引值为 1 的 Global,heap_base 是第二个 Global,还会有其他的一些变量按顺序依次排下去。

还有一种受管内存叫Locals。Locals在执行指令的时候,默认以当前的栈作为基础来进行访问和定位的。程序中基本类型的函数局部变量,可以使用 Locals 来映射,其他类型局部变量则会使用线性内存中 Aux Stack 来管理。和线性内存操作相比,Global 和 Local 操作目标的索引值是固定在 Wasm 文件中,说明其在编译期已然确定。而线性内存的地址是运行时才确定的。

最后还有一种叫做操作栈 (Operation Stack),Wasm字节码指令里会隐含操作栈访问,但没有任指令可以显式控制操作栈。此空间用来存储操作数和返回结果等数据。

最后

本文带大家认识了WebAssembly的执行过程和内存模型,希望大家能关注到WebAssembly快的原因,当然WebAssembly性能优势远不止这些,还有多线程和SIMD等技术支持,更进一步地希望大家明白WebAssembly不是银弹,高性能WebAssembly的程序需要高质量的代码,也可结合内存知识来实现。

欢迎关注公众号:江湖修行,第一时间和作者交流。

你可能感兴趣的:(wasm,javascript,前端,web)