添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
直爽的番茄  ·  AttributeError: ...·  11 月前    · 
失恋的薯片  ·  [特別篇] ...·  1 年前    · 
强健的鸭蛋  ·  java.io.IOException: ...·  1 年前    · 
鼻子大的日记本  ·  java - How to ...·  1 年前    · 
想出国的羽毛球  ·  html - Bootstrap - ...·  1 年前    · 
学习
实践
活动
专区
工具
TVP
写文章

如何使用 WebAssembly 和 JS 构建高性能应用程序

本文最初发布于Medium网站,经原作者授权由InfoQ中文站翻译并分享。

自计算机发明以来,原生应用程序的性能有了巨大的提升。相比之下,由于JavaScript最初并不是为提高速度而构建的,因此Web应用程序的运行速度曾经相当缓慢。但是,由于浏览器之间的激烈竞争以及JavaScript引擎(例如V8)的迅速发展,JavaScript在机器上的运行速度也变得非常快了。但是它仍然无法在速度上击败原生应用程序。这主要是由于JavaScript代码必须经过多个流程才能生成机器代码。

JS引擎花费的平均时间

随着WebAssembly的引入,现代Web为我们所知的一切都有望迎来变革。这项技术快如闪电。在这篇文章文章中,我们就来看一下什么是WebAssembly,以及如何将它与JavaScript集成以构建高性能应用程序。

什么是WebAssembly?

在深入了解WebAssembly之前,我们先来看一下什么是Assembly。

汇编(Assembly)是一种底层编程语言,与CPU架构的机器级指令有着非常紧密的联系。换句话说,它离机器可理解的代码(称为机器代码)只差一个转换过程。这种转换过程称为 汇编

顾名思义, WebAssembly 可以理解为Web的汇编。它是一种类似于汇编语言的底层语言,有着紧凑的二进制格式,使你能够以接近原生的速度运行Web应用程序。它还为C、C++和Rust等语言提供了编译目标,从而使客户端应用程序能够以接近原生的性能运行在Web上。

此外,WebAssembly 被设计为与 JavaScript 并存,而不是替代后者。使用 WebAssembly JavaScript API,你可以在两种语言之间来回交换代码,而不会出现任何问题。这样,你就可以获得同时具备 WebAssembly 的功能和性能,以及 JavaScript 的多功能和适应性的应用程序。这打开了一个 Web 应用程序的全新世界,我们可以在 Web 上运行很多原本不准备用于 Web 的代码和功能。

它带来了什么变化

Lin Clark 预测,2017 年推出的 WebAssembly 可能会让 Web 开发产业迎来全新的拐点。上一个拐点来自现代浏览器中引入的 JIT 编译,其使 JavaScript 的速度提高了近 10 倍。

JavaScript 性能

如果对比 WebAssembly 与 JavaScript 的编译过程,你会注意到前者剥离了几个步骤,剩下的都被缩减了。下图是两种语言编译过程的直观对比。

WebAssembly 与传统 Web 应用程序编译过程的近似对比

仔细对比两者,你会注意到 WebAssembly 中的重优化部分已被完全剥离。这主要是因为:编译器无需对 WebAssembly 代码做任何假设,因为数据类型之类的东西在代码中是显式展现的。

但 JavaScript 不是这样,因为 JIT 应该为运行代码做出假设,如果假设失败,则应重优化代码。

如何获取 WebAssembly 代码

WebAssembly 是一项伟大的技术,但是你该如何使用它的力量呢?

你有几种方法可用。

  • 从头开始编写 WebAssembly 代码——除非你非常了解它的基础知识,否则完全不建议这样做。
  • 从 C 编译为 WebAssembly
  • 从 C++ 编译为 WebAssembly
  • 从 Rust 编译为 WebAssembly
  • 使用 AssemblyScript 将 Typescript 的一个严格变体编译为 WebAssembly。对于不熟悉 C/C++ 或 Rust 的 Web 开发人员来说,这是一个不错的选项。
  • Wasm 还支持更多语言选项,后文会提到。

此外,还有 Emscripten 和 WebAssembly Studio 之类的工具可以帮助你完成上述过程。

JavaScript 的 WebAssembly API

为了充分利用 WebAssembly 的功能,我们必须将其与 JavaScript 代码集成在一起。这可以在 JavaScript WebAssembly API 的帮助下完成。

模块编译和实例化

WebAssembly 代码位于.wasm 文件中。该文件应编译为针对底层机器的机器码。你可以使用 WebAssembly.compile 方法来编译 WebAssembly 模块。收到已编译的模块后,可以使用 WebAssembly.instantiate 方法实例化已编译的模块。另外,你也可以将获取.wasm 文件获得的数组缓存传递到 WebAssembly.instantiate 方法中。这也可以,因为实例化方法有两个重载。

let exports;
fetch('sample.wasm').then(response =>
  response.arrayBuffer();
).then(bytes =>
  WebAssembly.instantiate(bytes);
).then(results => {
  exports = results.instance.exports;

上述方法的缺点之一是这些方法不能直接访问字节码,因此在编译 / 实例化 wasm 模块之前,需要采取额外的步骤将响应转换为 ArrayBuffer。 相比之下,我们可以使用 WebAssembly.compileStreaming/WebAssembly.instantiateStreaming 方法来实现上述功能,其优点是可以直接访问字节码,而无需将响应转换为 ArrayBuffer。

let exports;
WebAssembly.instantiateStreaming(fetch('sample.wasm'))
.then(obj => {
  exports = obj.instance.exports;

应注意,WebAssembly.instantiate 和 WebAssembly.instantiateStreaming 会返回实例以及已编译的模块,这些实例可用于快速启动模块的实例。

let exports;
let compiledModule;
WebAssembly.instantiateStreaming(fetch('sample.wasm'))
.then(obj => {
  exports = obj.instance.exports;
  //access compiled module
  compiledModule = obj.module;

导入对象

实例化 WebAssembly 模块实例时,可以选择传递一个导入对象,该对象将包含要导入到新创建的模块实例中的值。它们可以是 4 种类型。

  • 全局变量值
  • 函数
  • memory
  • table

导入对象可以视为提供给模块实例以帮助其完成任务的工具。如果未提供导入对象,则编译器将分配默认值。

全局变量

WebAssembly 允许你创建可从 JavaScript 和 WebAssembly 模块访问的全局变量实例。你可以导入 / 导出这些变量,并在一个或多个 WebAssembly 模块实例中使用它们。

你可以使用 WebAssembly.Global() 构造器创建一个全局实例。

const global = new WebAssembly.Global({
    value: 'i64',
    mutable: true
}, 20);

全局构造器接收两个参数。

  • 一个对象,包含描述全局变量的数据类型和可变性的属性。允许的数据类型为 i32、i64、f32 或 f64
  • 实际变量的初始值。此值应为参数 1 中提到的类型。例如,如果你声明类型为 i32,则变量应为 32 位整数。同样,如果你声明类型为 f64,则变量应为 64 位浮点数。

const global = new WebAssembly.Global({
    value: 'i64',
    mutable: true
}, 20);
let importObject = {
    js: {
        global
WebAssembly.instantiateStreaming(fetch('global.wasm'), importObject)

全局实例应传递到 importObject 上,以便在 WebAssembly 模块实例中访问它。

Memory

在实例化时,WebAssembly 模块将需要分配一个 memory 对象。该 memory 对象应与 importObject 一起传递。如果没能这样做,则 JIT 编译器将使用默认值自动创建一个 memory 对象并将其附加到实例。

附加到模块实例的 memory 对象只是一个 ArrayBuffer。只需使用索引值,即可轻松访问 memory。此外,由于它是简单的 ArrayBuffer,因此可以简单地在 JavaScript 和 WebAssembly 之间传递和共享值。

Table

WebAssembly Table 是一个可调整大小的数组,位于 WebAssembly 的 memory 之外。该 Table 的值都是函数引用。尽管这听起来很像 WebAssembly memory,但它们是不同的,主要区别在于 Memory 数组是原始字节,而 Table 数组是引用。

引入 Table 主要是为了提高安全性。

你可以使用 set()、grow() 和 get() 方法来操作 Table。

演示

在这个演示中,我将使用 WebAssembly Studio 应用程序将一个 C 文件编译为.wasm。你可以在 这里 查看演示。

我创建了一个函数来计算wasm 文件中一个数字的幂。我将必要的值传递给函数,并在JavaScript 中接收输出。

同样,我在wasm 中进行了一些字符串操作。需要注意wasm 没有字符串类型,因此它用的是ASCII 值。返回到JavaScript 的值将指向存储输出的memory 位置。由于memory 对象是ArrayBuffer,因此我要进行迭代,直到收到字符串中的所有字符为止。

JavaScript 文件

let exports;
let buffer;
(async() => {
  let response = await fetch('../out/main.wasm');
  let results = await WebAssembly.instantiate(await response.arrayBuffer());
  // let results = await WebAssembly.instantiateStreaming(fetch('../out/main.wasm'));
  let instance = results.instance;
  exports = instance.exports;
  buffer = new Uint8Array(exports.memory.buffer);
  findPower(5,3);
  printHelloWorld();
})();
const findPower = (base = 0, power = 0) => {
  console.log(exports.power(base,power));
const printHelloWorld = () => {
  let pointer = exports.helloWorld();
  let str = "";
  for(let i = pointer;buffer[i];i++){
    str += String.fromCharCode(buffer[i]);
  console.log(str);

C 文件

#define WASM_EXPORT __attribute__((visibility("default")))
#include <math.h>
WASM_EXPORT
double power(double number,double power_value) {
  return pow(number,power_value);