WebAssembly的未来:潜在新特性一览

WebAssembly开发团队的描述:

WebAssembly(或wasm)是一种适用于Web的可移植编译格式,提供更小的文件尺寸和更快的加载速度。

实际上,WebAssembly旨在成为高级语言的编译目标。目前可以使用C、C++、Rust、Go、Java、C#编译器(还有更多)来创建wasm模块。

WebAssembly模块以二进制的格式发送到浏览器,并在专有虚拟机上执行。这个虚拟机与JavaScript虚拟机共享资源,如内存和线程。WebAssembly模块总是与JavaScript代码一起使用,在必要的时候可以执行一些有用的操作。

目前正在进行中的很多提案旨在让WebAssembly成为更好的编译目标,并减少JavaScript“胶水”代码。

WebAssembly提案

WebAssembly提案的流程是分阶段的,从阶段1(不成熟)到阶段5(完成标准化)。以下是目前所有提案的清单(前4个阶段),每个提案都将在后面详细介绍。

第1阶段(功能提案)包括以文本格式自定义注解、主机绑定、尾调用、批量内存操作、ECMA模块集成、垃圾回收、异常处理、固定宽度SIMD、线程。第2阶段(规范提议)包括BigInt转换。第2阶段(实现)包括引用类型和返回多个值。第4阶段(标准化)包括导出和导入可变全局变量和有符号扩展操作。

引用类型

目前的WebAssembly类型系统还很小,只有四种数字类型。目前,如果要使用复杂类型(例如字符串、对象、数组、结构体),需要将它们序列化为线性内存,并提供它们所在位置的引用。这个提案对类型系统进行了扩展,添加了一个新的anyref类型,模块可以持有对主机环境对象的引用,也就是说,你可以将JS对象传给wasm模块。

通过anyref引用的对象对于wasm模块来说意义不是很大,关键在于模块可以持有在JS堆上分配的对象的引用,这意味着在wasm执行期间需要对这些引用进行跟踪。该提案被视为垃圾回收提案的垫脚石。

返回多个值

WebAssembly的虚拟机是基于栈的,操作对象在函数被调用之前是放在栈上的,函数会使用这些对象并替换为返回值。在目前的规范中,函数只能返回单个值。新的提案允许指令、函数和块返回多个值(例如,整数除法可以返回除数和余数)。

下面是一个简单的“swap”函数,它可以返回多个值(result i32 i32)。

(func $swap (param i32 i32) (result i32 i32)
 (get_local 1) (get_local 0)
)

导出和导入可变全局变量

目前,wasm支持全局变量和局部变量。全局变量可以被导入和导出,并与JS宿主共享,不过必须将它们定义为不可变的。

新的提案将WebAssembly.Global构造函数添加到JS API中,允许导出和导入可变的全局变量。该特性对于跨多个wasm模块共享状态来说非常有用。

有符号扩展操作

符号扩展是一种在保留符号的同时增加二进制数位数的方法。这个提案添加了少量符号扩展指令,例如i32.extend8_s——将有符号的8位整数扩展为32位整数。

FireFox已经实现了这个特性,并计划在9月发布。

BigInt转换

JavaScript只有一个数字类型,即IEEE 754浮点数——这种表示法存在一定的局限性。现在有一个新兴的标准,即增加对“大整数”的支持,目前处于TC39流程的第3阶段。最终确定后,将为开发人员提供任意精度的整数。

WebAssembly的四种数字类型之一是64位整数。新提案将提供完全的互操作,让JS 的BigInt和wasm的64位整数实现双向转换。

顺便提一下,现在已经有一个用于在JavaScript中执行64位运算的库,叫作long.js。最近,他们移除了基于JavaScript的实现,改用更简单的WebAssembly!

线程

JavaScript已经通过WebWorker实现了多线程,但是在worker之间只能使用postMessage进行较慢的消息传递。共享内存提案已经在TC39中定稿,并在2017年2月成为ECMAScript的一部分。共享数组缓冲区和原子性让线程之间共享数据变得更加容易。

WebAssembly的这个提案也是允许共享访问线性内存,并提供原子操作。但值得注意的是,提案并没有引入创建线程的机制(引起了很多争议),而是由宿主提供此功能,也就是我们熟悉的WebWorker。

我相信会有一些人对这个提案感到失望。不过,WebAssembly之所以能够快速发展到MVP版本,跟团队专注于简单性不无关系。利用成熟的宿主功能(WebWorker)是非常有意义的。

将来可能会添加原生线程,但会作为单独的提案,不过这可能还需要几年的时间!

固定宽度SIMD

单指令多数据流(SIMD)是一组支持矢量风格处理的指令,例如,将一个向量添加到另一个向量种。所有现代CPU都支持这些指令。有一个SIMD.js TC39提案,增加了很多128位类型,例如float32 x 4,以及相应的操作(如add、multiply),但最近删除了这部分内容,因为WebAssembly中已经添加了类似的功能。这样做是有道理的,因为这些是低级指令,而WebAssembly恰好是低级运行时。

WebAssembly的SIMD提案非常简单,为wasm添加一个新的128位类型,可以表示四个数字的向量,以及用于创建和操作这些新类型的简单指令集。这将为某些算法类别的性能带来改进。

异常处理

程序会在出现异常时中断控制流,异常会顺着调用栈向上传播,直到遇到合适的“catch”块。异常处理是大多数现代编程语言的共同特征,尽管Swift在早期版本中并不支持它们。

WebAssembly MVP在当前控制流指令中没有任何类似于异常处理的东西。因此不得不使用Emscripten这样的工具来模拟这个功能,但这是以牺牲性能为代价——目前,默认情况下捕获C++异常是关闭的,其他语言也面临类似的问题。

异常处理提案概述了构建一个与宿主环境“良好配合”的概念所涉及的大量复杂性。有趣的是,这是第二次尝试创建这个提案,可见他们目前面临的挑战有多严峻!

该提案不仅要向WebAssembly添加异常处理,更是要引入一种更通用的事件概念,看起来很像中断。当事件发生时,执行被暂停,在events处安插一个恰当的处理程序。

除了事件,可能还会添加标准的try/catch指令:

try block_type
  instruction*
catch
  instruction*
end

这将减少WebAssembly编译器的一些复杂性。

垃圾回收

大多数现代编程语言(不包括系统级语言)使用垃圾回收器进行内存管理。简而言之,垃圾回收器(GC)让开发人员无需过多考虑内存管理,他们可以创建对象、传递对象、在函数/变量之间共享对象,并且在不再使用这些对象时依靠GC来清理它们。

WebAssembly没有垃圾回收器。事实上,它没有任何可用于内存管理的工具,它只是为你提供了一块“内存”。不使用GC的编程语言仍然需要某种机制来管理内存分配,例如Rust使用了一个小型的WebAssembly优化分配器。

目前,需要垃圾回收器的编程语言没有其他选择,只能将GC编译为wasm,并将其作为二进制文件的一部分,例如AssemblyScript就在二进制文件中包含了一个“makeshift GC”。但这样会增加二进制文件的大小,同时GC算法的效率也会受到影响。缺少GC是Scala和Elm等语言还不支持编译成WebAssembly的原因。

这个提案将GC功能带到WebAssembly中。有趣的是,它不会有自己的GC,而是与宿主环境的GC集成。还有其它各种其他提案(宿主绑定、引用类型)旨在改进与宿主的互操作性,从而更容易共享状态和调用API。使用单个GC来管理内存会让这些变得更容易。

这个提案是一个重大变更,wasm的类型系统因此添加了很多新的东西,包括简单的元组、结构体和数组。还有一些讨论是关于添加字符串类型的。

在WebAssembly中使用GC是可选的,这样Rust/C++就可以使用内存分配器和线性内存。新类型将在新的WebAssembly堆上进行分配,尽管提案中未明确说明。我猜宿主堆也可以使用,但可能会带来很大的开销。

这个提案增加了很多新的指令,这是结构体的一个例子:

;; structures with fields
(type $point (struct (field $x f64) (field $y f64) (field $z f64)))
 
;; allocated with new
(call $g (new $point (i32.const 1) (i32.const 2) (i32.const 3)))
 
;; field accessors - type checked when validated
(load_field $point $x (get_local $p)

ECMA模块集成

ECMAScript模块(ESM)是一个相对较新的规范,所有主流浏览器现在都已支持。

目前,wasm模块是通过HTTP进行加载的,然后使用JS API进行实例化:

const req = fetch("./myModule.wasm");
 
const instance = await WebAssembly.instantiateStreaming(req)
instance.exports.foo()

这个提案引入了一种机制,可以通过ESM导入的方式来加载wasm模块:

import {foo} from "./myModule.wasm";
foo()

这让实例化wasm模块变得更简单,而更大的好处是它们成为JS模块图的一部分,也可以进行摇树优化(tree shaking)、捆绑、代码分割和ESM支持的其他优化。

批量内存操作

该提案增加了复制/填充线性内存区域的新操作。它们将为某些场景带来更好的性能。

尾调用

递归函数调用可能会导致很深的调用栈,会带来各种问题(性能、内存消耗、堆栈溢出的可能性)。通过尾调用优化,递归函数调用将被替换为迭代。这种技术对于函数式语言来说非常重要。为此,该提案引入了新的return_call指令。

宿主绑定

基于多种因素(wasm类型系统太过简单、缺少引用类型等等),WebAssembly与JavaScript宿主之间的当前接口非常有限。如果你想要编写一个操作DOM或使用其他浏览器API的wasm模块,必须编写大量“胶水”代码。

这个提案允许WebAssembly模块创建、传递、调用和操作JavaScript/DOM对象。它添加了一部分与宿主绑定相关的内容,其中包括用于描述绑定机制或接口的注解。

Rust已经有了一个工具,叫作wasm-bindgen,它的作用与该提案很相似。使用wasm-bindgen,你就可以轻松地跨越wasm和JS传递字符串等对象。该工具将绑定元数据添加到wasm模块中,并生成所需的JS胶水代码。

以文本格式自定义注解

wasm二进制格式支持将元数据添加到模块中。这个提案为文本格式也添加了类似的功能,这对宿主绑定来说非常有用。举个例子:

(module
  (func (export "f") (param i32 (@js unsigned)) ...) 
)

@js unsigned注解添加了用于生成宿主绑定的其他元数据。

结论

希望这些能让你对WebAssembly未来的发展方向有所了解。这些提案中的一些小改进可能很快就能完成,但大的改进可能需要几年时间才能完全实现。

查看英文原文:https://blog.scottlogic.com/2018/07/20/wasm-future.html

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注