常见问题解答

既然已经有了 asm.js,为什么还要创建新的标准?

… 尤其是在 pthreads(Mozilla pthreadsChromium pthreads)和 SIMD(simd.jsChromium SIMDsimd.js in asm.js)即将进入 JavaScript 的情况下。

WebAssembly 提供了两个主要好处

  1. WebAssembly 正在考虑的二进制格式可以比 JavaScript 解析快得多地进行本地解码(实验 显示快了 20 倍以上)。在移动设备上,大型编译代码可能只需 **解析** 就需要 20-40 秒的时间,因此本地解码(尤其是在结合其他技术(如 以实现比 gzip 更好的压缩)时)对于提供良好的冷启动用户体验至关重要。

  2. 通过避免同时使用 AOT-可编译性 和在没有 特定 asm.js 优化 的引擎上也能获得良好性能的 asm.js 约束,新的标准使得添加达到原生性能级别的 功能 :unicorn: 变得 **容易得多**。

当然,每个新标准都会引入新的成本(维护、攻击面、代码大小),这些成本必须通过优势来抵消。WebAssembly 通过设计允许(但不强制)浏览器在它 **现有的** JavaScript 引擎中实现 WebAssembly 来最大程度地降低成本(从而重复使用 JavaScript 引擎现有的编译器后端、ES6 模块加载前端、安全沙盒机制和其他支持的 VM 组件)。因此,在成本方面,WebAssembly 应该与大型的 JavaScript 新功能相当,而不是对浏览器模型的根本扩展。

比较两者,即使对于已经优化了 asm.js 的引擎来说,优势也超过了成本。

WebAssembly 的用例是什么?

WebAssembly 是针对 各种用例 设计的。

WebAssembly 可以进行填充吗?

我们认为可以。早期的 原型 具有演示 [12],表明将二进制 WebAssembly 格式解码为 asm.js 可以非常高效。随着 WebAssembly 设计的变化,已经有了对填充进行 更多 实验

总的来说,对于 WebAssembly 在浏览器中的快速采用,乐观情绪一直在增加,这很棒,但这降低了开发填充的动力。

事实上,将 WebAssembly 填充为 asm.js 的紧迫性较低,因为存在替代方案,例如,反向填充 - 将 asm.js 编译为 WebAssembly - 存在,它允许发布一个可以作为 asm.js 或 WebAssembly 运行的单一构建。也可以通过简单地在 emscripten 中 切换开关 来构建两个并行的 asm.js 和 WebAssembly 构建项目,从而完全避免客户端上的填充时间。对于非高性能代码,第三种选择是使用编译的 WebAssembly 解释器(例如 binaryen.js)。

但是,WebAssembly 填充仍然是一个有趣的想法,原则上应该是可行的。

WebAssembly 只是针对 C/C++ 程序员吗?

高级目标 中所述,为了实现最小可行产品,最初的重点是 C/C++

但是,通过 与 ES6 模块接口集成,Web 开发人员无需编写 C++ 即可利用其他人编写的库;重复使用模块化的 C++ 库可以像 使用来自 JavaScript 的模块 一样简单。

除了 MVP 之外,另一个 高级目标 是改进对 C/C++ 以外的语言的支持。这包括 允许 WebAssembly 代码分配和访问垃圾回收(JavaScript、DOM、Web API)对象 :unicorn:。即使在 GC 支持添加到 WebAssembly 之前,也可以将语言的 VM 编译为 WebAssembly(假设它是用可移植的 C/C++ 编写的),并且已经证明了这一点(123)。但是,“编译 VM”策略会增加分发代码的大小,会失去浏览器开发者工具的集成,可能会出现跨语言循环收集问题,并且会错过需要与浏览器集成的优化。

我可以使用哪些编译器来构建 WebAssembly 程序?

WebAssembly 最初侧重于 C/C++,并且正在上游 clang/LLVM 中开发新的、干净的 WebAssembly 后端,然后可以被基于 LLVM 的项目(如 EmscriptenPNaCl)使用。

随着 WebAssembly 的发展,它将支持 C/C++ 以外的更多语言,我们希望其他编译器也能支持它,即使是针对 C/C++ 语言,例如 GCC。WebAssembly 工作组发现更容易从 LLVM 支持开始,因为他们从 EmscriptenPNaCl 工作中积累了更多关于该工具链的经验。

我们希望专有编译器也能获得 WebAssembly 支持,但我们会让供应商谈论他们自己的平台。

WebAssembly 社区组 很乐意与更多编译器供应商合作,将他们的意见纳入 WebAssembly 本身,并与他们一起处理 ABI 事宜。

WebAssembly 会支持 Web 上的“查看源代码”吗?

是的!WebAssembly 定义了一个 文本格式,当开发者在任何开发者工具中查看 WebAssembly 模块的源代码时,该格式会被渲染出来。此外,文本格式的一个具体目标是允许开发者手动编写 WebAssembly 模块,以进行测试、实验、优化、学习和教学。事实上,通过删除所有 asm.js 验证所需的强制转换,WebAssembly 文本格式应该比 asm.js 更自然地阅读和编写。在浏览器之外,将文本和二进制文件相互转换的命令行和在线工具也将随时可用。最后,作为 WebAssembly 工具故事 的一部分,也正在考虑可扩展的源映射形式。

Emscripten 用户如何处理?

现有的 Emscripten 用户可以通过切换标志来选择将他们的项目构建为 WebAssembly。最初,Emscripten 的 asm.js 输出将被转换为 WebAssembly,但最终 Emscripten 会在整个管道中使用 WebAssembly。这种无缝过渡得益于 高级目标,即 WebAssembly 与 Web 平台良好集成(包括允许对 JavaScript 进行同步调用),这使得 WebAssembly 与 Emscripten 当前的 asm.js 编译模型兼容。

WebAssembly 试图取代 JavaScript 吗?

不!WebAssembly 被设计为 JavaScript 的补充,而不是替代品。虽然 WebAssembly 会随着时间的推移允许将许多语言编译到 Web 上,但 JavaScript 拥有巨大的动力,并将继续成为 Web 上的唯一、特权的(如 上面 所述)动态语言。此外,预计 JavaScript 和 WebAssembly 将以多种配置一起使用

  • 利用 JavaScript 将各个部分粘合在一起的完整编译的 C++ 应用程序。
  • 围绕主要 WebAssembly 控制的中心画布的 HTML/CSS/JavaScript UI,允许开发者利用 Web 框架的功能来构建可访问的、类似于 Web 原生的体验。
  • 大部分是 HTML/CSS/JavaScript 应用程序,只有几个高性能的 WebAssembly 模块(例如,绘图、仿真、图像/声音/视频处理、可视化、动画、压缩等,这些示例我们今天已经在 asm.js 中看到了),允许开发者像今天使用 JavaScript 库一样重复使用流行的 WebAssembly 库。
  • 当 WebAssembly 获得访问垃圾回收对象的能力 :unicorn: 时,这些对象将与 JavaScript 共享,而不是存在于它们自己的封闭世界中。

为什么不直接使用 LLVM 位代码作为二进制格式?

LLVM 编译器基础设施具有许多吸引人的特性:它有一个现有的中间表示(LLVM IR)和二进制编码格式(位代码)。它具有针对许多体系结构的代码生成后端,并且由一个大型社区积极开发和维护。事实上,PNaCl 已经使用 LLVM 作为其二进制格式的基础。但是,LLVM 被设计为满足的目标和需求与 WebAssembly 的目标和需求略有不同。

WebAssembly 对其指令集体系结构 (ISA) 和二进制编码有几个要求和目标

  • 可移植性:ISA 必须对每种机器体系结构都相同。
  • 稳定性:ISA 和二进制编码不能随时间改变(或只以可以保持向后兼容的方式改变)。
  • 小型编码:程序的表示应尽可能小,以便通过互联网传输。
  • 快速解码:二进制格式应该能够快速解压缩和解码,以便程序快速启动。
  • 快速编译:ISA 应该能够快速编译(适合 AOT 或 JIT 编译),以便程序快速启动。
  • 最小化 非确定性:程序的行为应该尽可能地可预测和确定性(并且应该在每种体系结构上都相同,这是对上面提到的可移植性要求的更强形式)。

LLVM IR 的设计初衷是为了简化编译器优化实现,并表示 C、C++ 和其他语言在多种操作系统和体系结构上所需的结构和语义。这意味着默认情况下 IR 并非可移植的(同一程序在不同体系结构上具有不同的表示形式),也不稳定(它会随着优化和语言要求的变化而改变)。它包含了大量用于实现中级编译器优化但对代码生成无用的信息(但这对代码生成实现者来说是一个很大的负担)。它还存在未定义行为(很大程度上类似于 C 和 C++),这使得某些类别的优化变得可行或更强大,但会导致运行时出现不可预测的行为。LLVM 的二进制格式(位码)是为 IR 的临时磁盘序列化而设计的,用于链接时优化,而不是为了稳定性或可压缩性(尽管它确实具有一些针对这两种特性的功能)。

这些问题都不是不可克服的。例如,PNaCl 定义了 IR 的一个小型的可移植 子集,具有减少的未定义行为,以及位码编码的稳定版本。它还采用了几种技术来提高启动性能。但是,每个定制、变通方案和特殊解决方案都意味着从通用基础设施中获得的收益更少。我们相信,通过借鉴我们在 LLVM 上的经验,并为我们的目标和需求设计 IR 和二进制编码,我们可以比采用为其他目的设计的系统做得更好。

请注意,此讨论适用于将 LLVM IR 用作标准化格式。LLVM 的 clang 前端和中级优化器仍然可以用于从 C 和 C++ 生成 WebAssembly 代码,并且将在其实现中使用 LLVM IR,类似于 PNaCl 和 Emscripten 今天的方式。

为什么没有具有宽松浮点语义的快速数学模式?

优化编译器通常具有快速数学标志,允许编译器放宽有关浮点的规则,以便更积极地优化。这可能包括假设 NaN 或无穷大不会出现,忽略负零和正零之间的区别,进行改变舍入方式或溢出发生时间的代数操作,或用更便宜的计算近似值替换运算符。

这些优化实际上引入了不确定性;无法确定代码的行为,除非知道优化器做出的具体选择。这在原生代码场景中通常不是一个严重的问题,因为所有不确定性在生成原生代码时都会被解决。由于大多数硬件没有浮点不确定性,开发人员有机会测试生成的代码,然后依靠它在所有用户中始终如一地运行。

WebAssembly 实现运行在用户侧,因此开发人员没有机会测试代码的最终行为。此级别的非确定性会导致分布式 WebAssembly 程序在不同实现中表现出不同的行为,或者随着时间的推移而发生变化。WebAssembly 在权衡利弊的情况下确实 存在一些不确定性,但快速数学标志被认为并不重要到足以包括在内。

  • 许多重要的快速数学优化发生在编译器的中级优化器中,在 WebAssembly 代码发出之前。例如,如果用户应用了适当的快速数学标志,则依赖于浮点重新关联的循环矢量化仍然可以在此级别完成,因此 WebAssembly 程序仍然可以享受这些好处。另一个例子是,编译器可以在 WebAssembly 程序中用浮点乘以倒数替换浮点除法,就像它们对其他平台所做的那样。
  • 中级编译器优化也可以通过在 WebAssembly 中的 JIT 库 中实现来增强。这将允许它们执行受益于具有 目标信息 和源程序语义信息(例如快速数学标志)的优化。例如,如果添加了比 128 位更宽的 SIMD 类型,则预计将有功能测试允许 WebAssembly 代码确定在给定平台上使用哪些 SIMD 类型。
  • 当 WebAssembly 添加 FMA 运算符 :unicorn: 时,将乘法和加法序列折叠到 FMA 运算符中将成为可能。
  • WebAssembly 不包含自己的数学函数,例如 sincosexppow 等。WebAssembly 对此类函数的策略是允许它们作为 WebAssembly 本身的库例程实现(请注意,x86 的 sincos 指令很慢且不精确,因此一般来说 nowadays 都会避免使用它们)。希望在 WebAssembly 上使用更快且不太精确的数学函数的用户,只需选择一个这样做的数学库实现即可。
  • WebAssembly 所具有的大多数单独的浮点运算符,已经在硬件中映射到单独的快速指令。例如,告诉 addsubmul 不用担心 NaN 并不会使它们更快,因为 NaN 在所有现代平台的硬件中都得到了快速且透明的处理。
  • WebAssembly 没有浮点陷阱、状态寄存器、动态舍入模式或信号 NaN,因此依赖于不存在这些特性的优化都是安全的。

mmap 呢?

The mmap 系统调用具有许多有用的特性。虽然这些特性都打包在 POSIX 中的一个重载系统调用中,但 WebAssembly 将此功能分解为多个运算符。

  • MVP 从使用 grow_memory 运算符扩展线性内存的能力开始;
  • 提议的 未来特性 :unicorn: 将允许应用程序更改从 0memory_size 的连续范围内的页面的保护和映射。

The mmap 中的一个重要功能是,上述列表中缺少了分配不连续的虚拟地址范围的能力。省略此功能的原因是

  • 上述功能足以允许用户级 libc 实现完整的、兼容的 mmap,其表现为非连续内存分配(但在幕后只是协调使用 memory_resizemprotect/map_file/map_shmem/madvise)。
  • 允许非连续虚拟地址分配的好处是,如果它允许引擎将 WebAssembly 模块的线性内存与同一进程中的其他内存分配交织在一起(为了减轻虚拟地址空间碎片)。但这有两个问题

    • 这种与无关分配的交织目前不允许进行有效的安全检查,以防止一个模块破坏其堆之外的数据(请参阅 #285 中的讨论)。
    • 这种交织将需要使分配变得不确定,而 WebAssembly 一般来说 试图避免 不确定性。

为什么有 wasm32 和 wasm64,而不是仅仅使用一个抽象的 size_t

那么,用于保存抽象的 size_t 的线性内存量也需要由抽象来确定,然后将线性内存地址空间划分为用于不同目的的段将更加复杂。每个段的大小将取决于其中存储了多少个 size_t 大小的对象。理论上这是可行的,但这会增加复杂性,并且应用程序启动时需要做更多工作。

此外,允许应用程序静态地知道指针大小可以使它们更积极地进行优化。当优化器具有指针位宽的完整信息时,它们可以更好地折叠和简化整数表达式。此外,了解各种类型的内存大小和布局可以让人知道各种指针类型中存在多少个尾部零。

此外,C 和 C++ 与抽象的 size_t 的概念存在深刻的冲突。诸如 sizeof 之类的结构需要在编译器的前端完全计算,因为它们可以参与类型检查。甚至在更早之前,通常会有预定义的宏来指示指针大小,允许代码在编译的最早阶段为指针大小进行专门化。一旦专门化完成,信息就会丢失,从而破坏了引入抽象的尝试。

最后,如果需要并且实际情况允许,仍然可以在将来添加抽象的 size_t

为什么有 wasm32 和 wasm64,而不是仅仅使用 8 字节来存储指针?

大量应用程序永远不需要 4 GiB 的内存。强制所有这些应用程序为它们存储的每个指针使用 8 字节将显著增加它们所需的内存量,并降低它们对重要硬件资源(例如缓存和内存带宽)的有效利用率。

这里所述的动机和性能影响应该与促使为 Linux 开发 x32 ABI 的动机和性能影响基本相同。

即使是 Knuth 也认为有必要在某个时刻给我们他的意见,关于 64 位指针的讨论

我将能够访问专有的平台 API(例如 Android/iOS)吗?

是的,但这取决于 WebAssembly 嵌入器。在浏览器内部,你将能够访问与通过普通 JavaScript 可以访问的相同 HTML5 和其他特定于浏览器的 API。但是,如果 wasm VM 由特定供应商作为 “应用程序执行平台” 提供,它可能会提供对例如 Android/iOS 的 专有平台特定 API 的访问。