25

AssemblyScript 如何帮助 WebAssembly 发挥潜力?

 4 years ago
source link: https://www.infoq.cn/article/rjMAghJkATjf2VxIMUeY
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 开发人员认为这个 编译器 尚未准备好应对一般用途。

复制代码

mkdirassemblyscript-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 看起来像这样:

复制代码

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

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

复制代码

exportfunctionadd(a: i32, b: i32): i32{
returna + b;
}

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

复制代码

npmrunasbuild

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

复制代码

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

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

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

复制代码

$ node
WelcometoNode.js v12.10.0.
Type".help"formore information.
> constadd= require('./index').add;
undefined
>add(3, 5)
8

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

添加监视脚本

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

复制代码

npm install --save-devonchange

将 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) {
returnfalse;
}

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

returntrue;
}

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

复制代码

functionisPrime(x: u32): bool{
if(x <2) {
returnfalse;
}

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

returntrue;
}

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

复制代码

npminstall--save-devbenchmark

创建 benchmark.js :

复制代码

constBenchmark =require('benchmark');

constassemblyScriptIsPrime =require('./index').isPrime;

functionisPrime(x){
for(leti =2; i < x; i++) {
if(x % i ===0) {
returnfalse;
}
}

returntrue;
}

constsuite =newBenchmark.Suite;
conststartNumber =2;
conststopNumber =10000;

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

复制代码

AssemblyScript isPrime x74.00ops/sec ±0.43% (76runs sampled)
JavaScript isPrime x61.56ops/sec ±0.30% (64runs sampled)
AssemblyScript isPrimeis~20.2% faster.

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

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

加载模块

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

复制代码

<!DOCTYPE html>
<html>
<head>
<metacharset="utf-8"/>
<title>AssemblyScript isPrime demo</title>
</head>
<body>
<formid="prime-checker">
<labelfor="number">Enter a number to check if it is prime:</label>
<inputname="number"type="number"/>
<buttontype="submit">Submit</button>
</form>

<pid="result"></p>

<scriptsrc="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-devstatic-server

将脚本添加到 package.json。

复制代码

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

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

结论

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK