WebAssembly 初识

1. 前言

WebAssembly 是 W3C 标准化组织制定的一个可移植、体积小、加载快并且兼容 Web 的全新格式。利用 WebAssembly 技术可以方便地将非 JavaScript 代码快速地“运行”在浏览器中,从而为前端场景提供了无限可能;此外,随着 WebAssembly 在开发者社区中越来越流行,也正在成为服务端以及云计算平台上的新锐。

作为本课程的开篇,本文首先介绍 WebAssembly 的发展历程,正所谓 "以史为镜,可以知兴替",从其历史演变中我们可以深入了解 WebAssembly 技术的来龙去脉,以及它的核心诉求和价值;接下来,我们会梳理和总结 WebAssembly 的使命和优势;最后,我们通过一个简单的 WebAssembly 浏览器应用示例,带领读者一起身临其境地感受下 WebAssembly 在 Web 环境中的真实使用体验。

2. WebAssembly 历史演变

WebAssembly 技术源于浏览器,其发展历程可以说是一部浏览器性能优化史。在 Web 前端领域,JavaScript 语言是编写运行在浏览器上的 Web 应用的首选;与此同时,Node.js 等非浏览器 JavaScript 运行时环境的出现, JavaScript 变得越来越流行;然而,各类应用随着功能逐渐复杂化,对性能的要求越来越高;而由于 JavaScript 语言本身的一些特性,已经很难满足日益增长的应用性能需求。

为了满足应用日益增长的性能需求,针对 JavaScript 语言本身缺陷带来的瓶颈优化,逐步形成了三个阶段性优化产物,他们分别是 asm.js、NaCl/PNaCl 以及 WebAssembly,正是这三个阶段的优化逐步推动了 WebAssembly 技术的发展。接下来,我们将逐一从这三个阶段来阐述其各自的诉求及其价值,从而了解 WebAssembly 起源及其目标和优势。

2.1 asm.js 阶段

JavaScript 是前端开发的首选语言,由于 JavaScript 本身是一种动态、弱类型编程语言;因此,只有在程序运行时才能确定执行上下文中对象具体数据类型;不仅如此,JavaScript 语言还允许同一个变量在不同时刻可以绑定不同类型的对象。JavaScript 的弱类型导致虚拟机只能在执行时刻进行类型推断,同时,其动态性又进一步导致当前的执行结果无法被复用,因为代码所执行的对象类型和逻辑都可能随时改变。

为了解决 JavaScript 语言自身带来的上述弊端,asm.js 便应运而生。asm.js 设计的出发点直指 JavaScript 语言的设计缺陷,asm.js 可以在 JavaScript 代码运行之前便确定程序中变量的具体类型;与此同时,它还进一步保证程序中变量的类型不会在程序运行的过程中发生改变。基于 asm.js 对弱类型和动态性的约束,虚拟机在执行过程中可以利用确定性的类型进行编译优化,并且编译结果可以复用而不需要重复执行相同源代码的编译优化过程,从而使得 Web 应用的运行效率有巨大的提升。

asm.js 标准始于 2013 年 8 月,它是 JavaScript 的一个严格子集, 是一种可用于编译器的低层级的、高效的目标语言。相较 JavaScript 而言,asm.js 使用了一 种名为 Annotation 的类型声明方式来对变量类型进行约束,其中 Annotation 的形式采用 | T 表示。如下述 asm.js 模块代码所示,赋值语句 n = n | 0 通过对变量与 0 进行使用"按位或"操作的声明方式,可以让虚拟机在解析 asm.js 代码时强制将该变量 n 视为一个 32 位的整数,并且该变量所能够存储的数据类型在运行过程中无法被更改。

function fast_fib_module(stdlib, foreign, heap) {
    "use asm";
    function fib(n) {
        n = n|0;
        if (n >>> 0 < 3) {
            return 1|0;
        }
        return (fib((n-1)|0) + fib((n-2)|0))|0;
    }
    return fib;
}

从整体上看,asm.js 模块是一个标准的 JavaScript 函数。在 asm.js 的模块语法规则中,不仅需要对函数所传入的参数进行类型声明,而且这些是必需且强制的;此外,asm.js 标准中只定义了对数值类型的 Annotation 声明方式,这使得 asm.js 的应用场景很大程度上集中在数值计算密集型 Web 应用的优化处理上;再加上各大浏览器厂商对 asm.js 标准的支持程度和实现方式也不尽相同。

asm.js 技术本身存在的问题和局限性,大大阻碍了 asm.js 的发展,从而也推动了 Web 应用优化进入了 NaCl 和 PNaCl 阶段。

2.2 NaCl 和 PNaCl 阶段

NaCl(Google Native Client) 是 2011 Google Chrome 团队意在把基于原生 C/C++ 语言编写的本地应用安全、高效地运行在 Web 浏览器端的一项技术。该技术主要方式是将原生 C/C++ 语言编写的本地应用编译为标准的 NaCl 模块,每个 NaCl 模块是一个以 ".nexe" 为后缀的 ELF 格式二进制文件,该文件可以直接在 Chrome 中加载并运行。基于 NaCl 技术开发出来的应用可以以接近原生 C/C++ 应用的效率在浏览器端稳定地运行,但在实际项目使用时,需要为每种不同的处理器架构分别单独编译对应版本的 NaCl 二进制模块文件;这种方式既不方便也不符合开源软件的便携特性;不仅如此,NaCl 模块这种直接存储针对底层处理器架构机器码的方式也使得模块本身失去了可移植性。

为了解决由于 NaCl 模块的平台依赖性导致的互联网上自由地分发的问题,又推出了新的 PNaCl (Chrome Portable Native Client) 技术。PNaCl 并不会直接将应用的 C/C++ 源代码编译成依赖特定处理器架构的底层机器码,PNaCl 首先会将应用的 C/C++ 源代码编译成一种基于 LLVM 生成的抽象中间二进制模块 ,以 ".pexe" 为后缀。这种模块不依赖具体的处理器架构,因此可以在互联网上被随意地分发。在浏览器中运行 PNaCl 应用时,浏览器会首先将 pexe 二进制模块加载到内存中,并根据当前处理器架构通过内置的 AOT 编译器将 pexe 二进制模块编译为特定处理器架构的机器码,随后被浏览器直接执行。

NaCl 和 PNaCl 技术没有被除 Chrome 以外的任何其他浏览器支持;此外,基于 C/C++ 语言也大大增加了 NaCl 和 PNaCl 应用的开发难度和开发成本,使得 NaCl 和 PNaCl 基本脱离了技术快速迭代的前端开发领域。

NaCl 和 PNaCl 技术本身存在的问题和局限性,阻碍了它成为浏览器的事实标准,从而也推动了 Web 应用优化进入到了当前的 WebAssembly 阶段。

2.3 WebAssembly 阶段

基于前两个阶段的探索,为了提升 Web 应用的性能,2015 年 Mozilla 在 asm.js 的基础上发布了一种新型的二进制代码格式 "WebAssembly"。这种二进制文件可以用类似 JavaScript 模块加载的方式被浏览器快速、 高效地执行;不仅如此,WebAssembly 并不像 NaCl 那样需要区分浏览器所运行的处理器架构,这使得它可以自由地在互联网上分发,具有很好的浏览器兼容性和用户体验。鉴于 WebAssembly 的优势,2017 年,Firefox、Chrome、Edge 和 Webkit 四大浏览器厂商在 WebAssembly MVP (最小可用版本) 标准的设计上达成共识;同年,WebAssembly Working Group (以下简称 WWG) 成立,标志着 WebAssembly 成为 W3C 标准技术体系的一部分;2019 年 12 月,宣布 WebAssembly 成为第 4 种 Web 语言,同时 WebAssembly 1.0 标准正式落地。

随着 WebAssembly 在开发者社区中越来越流行,WebAssembly 的潜在价值从 Web 逐渐开始向其他领域,比如云原生、AI 以及区块链等;2019 年 12 月,Bytecode Alliance 字节码联盟宣布正式成立,联盟旨在通过协作的方式,来共同实现 WebAssembly 及 WASI 相关标准,并通过提出新标准的方式来共同打造 WebAssembly 在浏览器之外的未来生态。

在最近的 2022 年, WebAssembly 2.0 草案正式发布,相比 WebAssembly 1.0,WebAssembly 2.0 草案中加入了很多值得关注的新特性,比如引用类型(Reference Types)、固定宽度的 SIMD(Fixed-width SIMD)、批量内存操作(Bulk Memory Operations)等等。

3. WebAssembly 目标及其优势

我们在上一节中介绍了 WebAssembly 发展历程,其设计的主要目标也是其核心优势主要体现在如下几个方面:

图 1. 传统应用开发、运行模式

传统开发模式一般如图 1 所示,通过特定的语言生态开发、发布和运行应用。这种模式在大型系统中不可避免的暴露出其劣势:

WebAssembly 为应用开发者提供了不一样的技术架构选型的可能性。WebAssembly 作为一个可安全隔离,高效,体积小、跨平台,多语言支持的可移植二进制中间形式,为解决上述问题提供了可实施的路径,不同的开发者可以利用 WebAssembly 的特性来满足业务需求。

对于 JavaScript、Python 等脚本语言来说,为了追求更高的性能,可以将性能热点模块通过 WebAssembly 来实现,从而获取高性能执行的收益。对于 Rust 开发者来说,利用语言的特性可以获取高性能和高安全性,但为了让开发者获得更低的开发门槛,可以编译为 WebAssembly 模块提供给类似 JavaScript、Python 等脚本语言使用,降低开发者门槛;对于 C++ 开发者来说,可以获得高性能,但 C++ 不完备的安全性机制可能会使应用存在安全隐患,可以将其编译为 WebAssembly 在轻量级安全沙箱中运行,从而使得安全机制做到开箱即用(安全性保障需要安全领域的专业支持,门槛很高),如图 2 所示。

图 2. WebAssembly 多语言使用场景示例

4. WebAssembly Web 应用体验

在前面各小节中,我们已经一起了解了 WebAssembly 技术及其特点,为了更直观地了解 WebAssembly 的优势,本小节将搭建 Mandelbrot Web 应用来展示 WebAssembly 是如何与 JavaScript 语言结合,以及如何无缝的在 Web 环境中运行。其中,Mandelbrot [8] 是在复平面上组成分形的点的集合,其图形绘制过程需要进行大量的复平面数值计算,通过 WebAssembly 实现可以有效的提高计算和绘制性能。Mandelbrot 图形绘制算法将采用 AssemblyScript [9] 来编写,并通过 AssemblyScript 编译器生成 WebAssembly 二进制模块。

本小节的剩余部分,我们将详解介绍 Mandelbrot Web 应用的创建过程和运行效果。

  1. 为了能够将 AssemblyScript 编译为 WebAssembly 二进制模块,需要通过如下命令创建项目工程,并预先安装 AssemblyScript 编译器。
npm init
npm install --save-dev assemblyscript
  1. Mandelbrot 图形绘制函数 computeLine 保存为 Mandelbrot.ts,当完成工程创建后,通过如下命令行可以将 Mandelbrot.ts 编译为 WebAssembly 二进制文件 Mandelbrot.wasm。
npx asc Mandelbrot.ts --target release -o Mandelbrot.wasm
// file: Mandelbrot.ts

/** Number of discrete color values on the JS side. */
const NUM_COLORS = 2048;

/** Computes a single line in the rectangle `width` x `height`. */
export function computeLine(y: u32, width: u32, height: u32, limit: u32): void {
    var translateX = width * (1.0 / 1.6);
    var translateY = height * (1.0 / 2.0);
    var scale = 10.0 / min(3 * width, 4 * height);
    var imaginary = (y - translateY) * scale;
    var realOffset = translateX * scale;
    var stride = (y * width) << 1;
    var invLimit = 1.0 / limit;

    var minIterations = min(8, limit);

    for (let x: u32 = 0; x < width; ++x) {
        let real = x * scale - realOffset;

        // Iterate until either the escape radius or iteration limit is exceeded
        let ix = 0.0, iy = 0.0, ixSq: f64, iySq: f64;
        let iteration: u32 = 0;
        while ((ixSq = ix * ix) + (iySq = iy * iy) <= 4.0) {
            iy = 2.0 * ix * iy + imaginary;
            ix = ixSq - iySq + real;
            if (iteration >= limit) break;
            ++iteration;
        }

        // Do a few extra iterations for quick escapes to reduce error margin
        while (iteration < minIterations) {
            let ixNew = ix * ix - iy * iy + real;
            iy = 2.0 * ix * iy + imaginary;
            ix = ixNew;
            ++iteration;
        }

        // Iteration count is a discrete value in the range [0, limit] here, but we'd like it to be
        // normalized in the range [0, 2047] so it maps to the gradient computed in JS.
        // see also: http://linas.org/art-gallery/escape/escape.html
        let col = NUM_COLORS - 1;
        let sqd = ix * ix + iy * iy;

        if (sqd > 1.0) {
            let frac = Math.log2(0.5 * Math.log(sqd));
            col = ((NUM_COLORS - 1) * clamp((iteration + 1 - frac) * invLimit, 0.0, 1.0));
        }
        store(stride + (x << 1), col);
    }
}

/** Clamps a value between the given minimum and maximum. */
@inline
function clamp(value: T, minValue: T, maxValue: T): T {
    return min(max(value, minValue), maxValue);
}
  1. 为了展示 Mandelbrot 效果,可以通过在 Web 页面[11] 中来加载 Mandelbrot.wasm,并调用 computeLine 函数来计算 Mandelbrot 集来完成图形绘制。
// file: index.html

// Set up the canvas with a 2D rendering context
var cnv = document.getElementsByTagName("canvas")[0];
var ctx = cnv.getContext("2d");
var bcr = cnv.getBoundingClientRect();

// Compute the size of the viewport
// var width = bcr.width | 0;
// var height = bcr.height | 0;
// var ratio = window.devicePixelRatio || 1
// ...
// ctx.scale(ratio, ratio);

// Compute the size of and instantiate the module's memory
const memory = new WebAssembly.Memory({
    initial: ((byteSize + 0xffff) & ~0xffff) >>> 16
});
const mem = new Uint16Array(memory.buffer);
const imageData = ctx.createImageData(width, height);
const argb = new Uint32Array(imageData.data.buffer);

// Fetch and instantiate the module
fetch("build/Mandelbrot.wasm")
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, {
    env: {
        memory,
        "Math.log": Math.log,
        "Math.log2": Math.log2
    },
}))
.then(module => {
    const exports = module.instance.exports;
    const computeLine = exports.computeLine;
    const updateLine = function (y) {
        var yx = y * width;
        for (let x = 0; x < width; ++x) {
            argb[yx + x] = colors[mem[yx + x]];
        }
    };

    // Compute an initial balanced version of the set.
    const limit = 40;
    for (let y = 0; y < height; ++y) {
        computeLine(y, width, height, limit);
        updateLine(y);
    }

    // Keep rendering the image buffer.
    (function render() {
        if (animate) requestAnimationFrame(render);
        ctx.putImageData(imageData, 0, 0);
    })();
    // ...
}).catch(err => {
    alert("Failed to load WASM: " + err.message + " (ad blocker, maybe?)");
    console.log(err.stack);
});
  1. 按上述步骤,我们已经生成 Mandelbrot.wasm 二进制模块,并在 index.html 中完成模块加载和图形绘制的逻辑实现,最后,我们可以在本地建立 http 服务来加载和展示 Mandelbrot 图形效果,如图 3 所示。
cd $webassembly_tech/samples/mandelbrot
npx serve

> ┌──────────────────────────────────────────┐
> │  Serving!                                │
> │                                          │
> │   - Local:    http://localhost:3000      │
> │                                          │
> │   Copied local address to clipboard!     │
> │                                          │
> └──────────────────────────────────────────┘

图 3. Mandelbrot 集图形效果

虽然上述步骤中,我们已经详细描述了 Mandelbrot Web 应用中 WebAssembly 的生成和 Web 页面集成的各个步骤;但为了方便读者构建原型,webassembly_tech [11] 提供了源代码的下载,并提供了 Mandelbrot 项目使用文档快速构建和查看效果。

5. 总结

至此,我们已经完整地介绍了 WebAssembly 技术的来龙去脉以及它的核心价值,并通过 Mandelbrot 示例带领读者一起身临其境地感受了 WebAssembly 在 Web 环境中的真实使用体验。然而,随着 WebAssembly 技术的不断发展,其应用领域和应用场景也越来越广阔,为了更好的了解 WebAssembly 的价值及其应用生态,在接下来的第二章中,我们将会对 WebAssembly 使用场景和未来发展趋势做进一步的介绍和探索。

6. 参考文献

[1]. "asm.js" Working Draft: http://asmjs.org/spec/latest/
[2]. Native Client (NaCl & PNaCl): https://www.chromium.org/nativeclient/
[3]. WebAssembly: https://webassembly.org/specs/
[4]. Bringing WebAssembly outside the web with WASI by Lin Clark: https://www.youtube.com/watch?v=fh9WXPu0hw8
[5]. Mandelbrot set: https://en.wikipedia.org/wiki/Mandelbrot_set
[6]. Local Web Server: https://github.com/yaozhongxiao/cli/tree/master/server
[7]. webassembly_tech: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples
[8]. Mandelbrot set: https://en.wikipedia.org/wiki/Mandelbrot_set
[9]. AssemblyScript: https://www.assemblyscript.org/
[10]. Mandelbrot Set:https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/mandelbrot/index.html
[11]. webassembly_tech: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/mandelbrot/README.md


作者:姚忠孝

来源:微信公众号:字节前端 ByteFE

出处:https://mp.weixin.qq.com/s/xvhQxqF5pWUVleL_P3tqgA

展开阅读全文

页面更新:2024-04-04

标签:变量   虚拟机   浏览器   性能   语言   类型   价值   代码   程序   技术

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top