AssemblyScript如何帮助WebAssembly发挥潜力?

WebAssembly(或Wasm)是Web浏览器中相对较新的功能,但它有潜力极大地扩展Web作为一个应用程序服务平台的能力。Web开发人员在入门WebAssembly时可能会经历艰难的学习过程,而AssemblyScript就提供了一种解决办法。首先我们来看一下为什么WebAssembly是一项很有前途的技术,然后再介绍AssemblyScript是怎样帮助WebAssembly发挥潜力的。

WebAssembly

WebAssembly是针对浏览器使用的底层语言,为开发人员提供了JavaScript之外的Web编译目标。它使网站代码可以在安全的沙盒环境中以接近原生的速度运行。

它是根据所有主流浏览器(Chrome、Firefox、Safari和Edge)代表的意见开发的,这些代表于2017年初达成了设计共识。所有这些浏览器现在都支持WebAssembly,意味着整个市场中约87%的浏览器可以使用它。

WebAssembly以二进制格式交付,这意味着与JavaScript相比,WebAssembly在大小和加载时间上均占优势。但它也有供人类阅读的文本表示形式

当WebAssembly首次亮相时,一些开发人员认为它最后有可能取代JavaScript,成为Web的主要语言。但最好将WebAssembly视为与现有Web平台集成良好的一项新工具,这也是其高阶目标之一

WebAssembly并没有取代现有的JavaScript用例,而是开拓了新的用户场景,吸引了更多人的兴趣。WebAssembly尚不能直接访问DOM,并且大多数现有网站都希望继续使用JavaScript——毕竟经过多年的优化,JavaScript已经相当快了。下面是WebAssembly自身提供的可行用例列表:

  • 游戏
  • 科学计算的可视化和模拟
  • CAD应用
  • 图像/视频编辑

这些用例的共同属性是,我们通常会将它们视为桌面应用程序。WebAssembly可以为CPU密集型任务提供接近原生平台的性能表现,这样人们就能将更多桌面型应用程序移植到Web端了。

现有网站也可以从WebAssembly中受益。Figma提供了一个现实应用的示例,它使用WebAssembly大大缩短了加载时间。如果网站使用的某些代码需要进行大量的计算,则可以只将这部分代码替换为WebAssembly以提高性能。

所以也许现在你就有兴趣开始使用WebAssembly了。你可以学习这种语言本身并直接编写它,但实际上它打算成为其他语言的编译目标。它被设计为对C和C++具有良好的支持,Go在1.11版中添加了对它的实验性支持,Rust也对其投入了大量资源

但也许你并不想为了使用WebAssembly而学习或使用其中的任何一种语言。这就轮到AssemblyScript出场表现了。

AssemblyScript

AssemblyScript是一个TypeScript到WebAssembly的编译器。由Microsoft开发的TypeScript为JavaScript添加了类型。它已经非常流行了,但就算用户不怎么熟悉TS,AssemblyScript也只支持TypeScript功能的一个有限子集,因此不需要花很长时间就能上手。

因为它与JavaScript非常相似,所以AssemblyScript使Web开发人员可以轻松地将WebAssembly整合到他们的网站中,而不必使用某种完全不同的语言。

尝试一下

下面我们试着编写第一个AssemblyScript模块(所有代码都在这个GitHub仓库中提供:https://github.com/dguo/assemblyscript-demo)。为了支持WebAssembly,我们需要的Node.js最低版本是8.0

转到一个空目录,创建一个package.json文件,然后安装AssemblyScript。请注意,我们需要直接从其GitHub仓库安装它。它尚未在npm上发布,因为AssemblyScript开发人员认为这个编译器尚未准备好应对一般用途。

mkdir assemblyscript-demo
cd assemblyscript-demo
npm init
npm install --save-dev github:AssemblyScript/assemblyscript

使用随附的asinit命令生成脚手架文件:

npx asinit .

我们的package.json现在应该包含以下脚本:

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"
  }
}

顶层index.js看起来像这样:

const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
  env: {
    abort(_msg, _file, line, column) {
       console.error("abort called at index.ts:" + line + ":" + column);
    }
  }
};
Object.defineProperty(module, "exports", {
  get: () => new WebAssembly.Instance(compiled, imports).exports
});

它使我们可以轻松地require我们的WebAssembly模块,就像require普通的JavaScript模块一样。其中,assembly目录包含我们的AssemblyScript源代码。生成的示例是一个简单的加法函数。

export function add(a: i32, b: i32): i32 {
  return a + b;
}

你可能以为函数签名会像TypeScript中的形式,也就是add(a: number, b: number): number这种格式;但这里之所以使用i32,原因是AssemblyScript使用了WebAssembly的特定整数和浮点类型,而不是TypeScript的通用数字类型。下面我们来构建示例。

npm run asbuild

现在,build目录应包含以下文件:

optimized.wasm
optimized.wasm.map
optimized.wat
untouched.wasm
untouched.wasm.map
untouched.wat

我们得到了构建的普通版本和优化版本。对于每个构建版本,我们都有了一个.wasm二进制文件、一个.wasm.map源映射,以及该二进制文件的.wat文本表示形式。文本表示形式是用来供人类阅读的,但在这个例子中我们无需阅读或理解它——使用AssemblyScript的其中一个目的,就是用不着使用原始的WebAssembly了。

启动Node,并像其他模块一样使用我们的编译模块。

$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> const add = require('./index').add;
undefined
> add(3, 5)
8

从Node调用WebAssembly就只需要这些步骤!

添加监视脚本

在开发时,建议你在更改源代码时使用onchange自动重建模块,因为AssemblyScript尚不包含监视模式

npm install --save-dev onchange

将asbuild:watch脚本添加到package.json。加入-i标志,可在运行命令后立即运行初始构建。

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
    "asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild"
  }
}

现在你可以运行asbuild:watch,这样就用不着不断重新运行asbuild了。

性能

我们来写一个基本的基准测试,看看我们可以获得怎样的性能提升。WebAssembly的专长是处理诸如数字计算之类的CPU密集型任务,所以我们这里使用一个函数来确定一个整数是否为质数。

我们的参考实现如下所示。这是一种原始的暴力解决方案,因为我们的目标是执行大量计算。

function isPrime(x) {
    if (x < 2) {
        return false;
    }

    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

等效的AssemblyScript版本仅需要一些类型注释:

function isPrime(x: u32): bool {
    if (x < 2) {
        return false;
    }

    for (let i: u32 = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

我们将使用Benchmark.js(https://benchmarkjs.com/)。

npm install --save-dev benchmark

创建benchmark.js :

const Benchmark = require('benchmark');

const assemblyScriptIsPrime = require('./index').isPrime;

function isPrime(x) {
    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

const suite = new Benchmark.Suite;
const startNumber = 2;
const stopNumber = 10000;

在我的机器上,运行node benchmark时得到了以下结果:

AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled)
JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled)
AssemblyScript isPrime is ~20.2% faster.

请注意,这个测试是一个microbenchmark,我们不应该太看重它的结果。

如果你想要参考一些更深度的AssemblyScript基准测试,我建议了解WasmBoy基准测试wave equation基准测试

加载模块

接下来我们在网站中使用我们的模块。创建index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>AssemblyScript isPrime demo</title>
    </head>
    <body>
        <form id="prime-checker">
            <label for="number">Enter a number to check if it is prime:</label>
            <input name="number" type="number" />
            <button type="submit">Submit</button>
        </form>

        <p id="result"></p>

        <script src="demo.js"></script>
    </body>
</html>

创建demo.js。要加载WebAssembly模块有多种方法,但最有效的方法是使用WebAssembly.instantiateStreaming函数,以流方式编译和实例化这些模块。请注意,我们需要提供一个中止函数,如果断言失败就会调用这个中止函数。

(async () => {
    const importObject = {
        env: {
            abort(_msg, _file, line, column) {
                console.error("abort called at index.ts:" + line + ":" + column);
            }
        }
    };
    const module = await WebAssembly.instantiateStreaming(
        fetch("build/optimized.wasm"),
        importObject
    );
    const isPrime = module.instance.exports.isPrime;

    const result = document.querySelector("#result");
    document.querySelector("#prime-checker").addEventListener("submit", event => {
        event.preventDefault();
        result.innerText = "";
        const number = event.target.elements.number.value;
        result.innerText = `${number} is ${isPrime(number) ? '' : 'not '}prime.`;
    });
})();

现在安装static-server。我们需要一个服务器,因为使用WebAssembly.instantiateStreaming时,该模块需要MIME类型的application/wasm。

npm install --save-dev static-server

将脚本添加到package.json。

{
  "scripts": {
    "serve-demo": "static-server"
  }
}

运行npm run serve-demo命令,并在浏览器中打开localhost URL。在表单中提交一个数字,你将收到一条消息,指出该数字是否为质数。到这里,从编写AssemblyScript,到在网站中实际使用它的整个流程我们都走了一遍。

结论

WebAssembly和它的AssemblyScript扩展并不会一夜之间加快所有网站的速度,但这也不是它们的目的。WebAssembly之所以令人兴奋,是因为它为Web开拓了更多的可能性,从而支持更多种类的应用程序。

类似地,AssemblyScript使更多开发人员可以快速上手WebAssembly,这样我们就能在一般场景中继续使用JavaScript,而在需要大量数字运算的任务中轻松切换到WebAssembly了。

原文链接
https://blog.logrocket.com/the-introductory-guide-to-assemblyscript/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章