【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

如何使用 WebAssembly 提升性能

  • 2019-04-23
  • 本文字数:6351 字

    阅读完需:约 21 分钟

如何使用WebAssembly提升性能

在之前的文章中,我谈到了如何借助 WebAssembly 将 C/C++库生态系统引入到 Web 中。squoosh是一个广泛使用了 C/C++库的 Web 应用程序,使用各种从 C++编译为 WebAssembly 的 codec 来压缩图像。


WebAssembly 是一种低级虚拟机,可以保存在.wasm 文件中的字节码。这种字节代码是强类型和结构化的,在宿主系统上经过编译很优化后可以运行得比 JavaScript 更快。


根据我的经验,Web 的大多数性能问题都是由强制布局和过多的绘制引起的,偶尔也需要执行一些耗时的高计算成本任务,而 WebAssembly 在这个时候就可以派上用场。

热路径

在 squoosh 中,我们提供了一个JavaScript函数,用于将图像缓冲区旋转 90 度。虽然OffscreenCanvas也可以用来实现这个功能,但它并不支持我们的所有目标浏览器,而且在 Chrome 中还有一些小 bug。


这个函数迭代输入图像的每个像素,并将它们复制到输出图像的不同位置,以此来实现旋转。对于一张 4094×4096 像素的图像(1600 万像素),它需要进行 1600 万次内部代码块迭代,也就是我们所说的“热路径”。尽管迭代次数很多,但我们测试的三个浏览器中有两个可以在 2 秒或更短时间内完成迭代。对于这种迭代任务,这样的时间是可接受的。


for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {  for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));    outBuffer[i] = inBuffer[in_idx];    i += 1;  }}
复制代码


但是,其中有一个浏览器需要 8 秒钟。浏览器优化 JavaScript 的方式非常复杂,不同的引擎所优化的东西是不一样的。有一些针对原始执行进行了优化,有一些针对与 DOM 的交互进行了优化。因此,在一些浏览器中,我们遇到了未经优化的路径。


WebAssembly 完全是围绕提升原始执行速度而构建的。因此,如果我们希望这些代码能够获得快速、可预测的跨浏览器性能,可以考虑使用 WebAssembly。

WebAssembly 实现可预测的性能

通常,JavaScript 和 WebAssembly 可以达到相同的峰值性能。但是,对于 JavaScript 来说,这种性能只能在“快速路径”上实现,而且要保持在“快速路径”上并不容易。WebAssembly 的一个主要优势是可预测的性能,即使是跨浏览器也是如此。严格的类型和低级架构可以让编译器做出更强的保证,只需要对 WebAssembly 代码优化一次,就可以始终使用“快速路径”。


之前我们使用了 C/C++库,并将它们编译为 WebAssembly,以便在 Web 上使用它们。但实际上,我们并没有真正触及库的代码,我们只是写了少量的 C/C++代码作为浏览器和库之间的桥梁。但这次不一样:我们想要从头开始写一些东西,以便利用 WebAssembly 的优势。

WebAssembly 架构

在开始写代码之前,有必要先了解一下 WebAssembly。


引用 WebAssembly.org 的话:


WebAssembly(缩写为 Wasm)是一种用于栈虚拟机的二进制指令格式。Wasm 被设计为一个可移植的目标,用于编译 C/C++/Rust 等高级语言,支持在 Web 上部署客户端和服务器应用程序。


在将一段 C 语言或 Rust 代码编译为 WebAssembly 后,会得到一个包含模块声明的.wasm 文件。声明中包含了一个导入列表、一个导出列表(函数、常量、内存块)和函数的二进制指令。


有一些需要注意的东西:WebAssembly 虚拟机栈并没有保存在 WebAssembly 模块所使用的内存块中。虚拟机栈完全处在虚拟机内部,Web 开发人员无法访问它(除了通过 DevTools)。因此,我们可以编写完全不需要任何额外内存(只是有虚拟机内部栈)的 WebAssembly 模块。


在我们的例子中,我们需要使用一些额外的内存来访问图像的像素并生成图像的旋转版本。这个时候要用到 WebAssembly.Memory。

内存管理

通常,一旦你使用了额外的内存,就需要以某种方式来管理内存。内存的哪些部分正在使用中?哪些部分是可用的?例如,C 语言提供了 malloc(n)函数,用来查找连续 n 个字节的内存空间。这种功能也被称为“分配器”。分配器需要被包含在 WebAssembly 模块中,这样会增加文件的大小。根据算法的不同,这些内存管理功能的体积和性能可能会有很大差异,这就是为什么很多语言提供了多种实现(“dmalloc”、“emmalloc”、“wee_alloc”……)。


在我们的例子中,在运行 WebAssembly 模块之前,我们知道输入图像的尺寸(以及输出图像的尺寸)。通常我们会将输入图像的 RGBA 缓冲区作为参数传给 WebAssembly 函数,并将旋转后的图像作为值返回。要生成这个返回值,我们需要使用分配器。但因为我们知道所需的内存总量(输入图像大小的两倍,一次用于输入,一次用于输出),所以可以使用 JavaScript 将输入图像放入 WebAssembly 内存,运行 WebAssembly 模块生成旋转图像,然后使用 JavaScript 回读结果。这样我们就可以不使用内存管理!


https://storage.googleapis.com/webfundamentals-assets/hotpath-with-wasm/animation_2_vp8.webm


如果你看一下原始的 JavaScript 函数,你会发现它其实是一些纯粹的计算代码,没有使用特定的 JavaScript API,所以可以很容易地将这些代码移植到其他语言。我们评估了 3 种可编译为 WebAssembly 的语言:C/C++、Rust 和 AssemblyScript。我们唯一要解决的问题是:如何在不使用内存管理功能的情况下访问原始内存?

C 语言和 Emscripten

Emscripten 是用于将 C 语言编译成 WebAssembly 的编译器。Emscripten 的目标是成为 GCC 或 clang 等知名 C 语言编译器的直接替代品。这是 Emscripten 的核心任务,它旨在尽可能简单地将现有 C 语言和 C++代码编译为 WebAssembly。


访问原始内存是 C 语言的本质,指针的存在就是为了这个:


uint8_t* ptr = (uint8_t*)0x124;ptr[0] = 0xFF;
复制代码


我们将数字 0x124 转换为指向无符号 8 位整数(或字节)的指针,将 ptr 变量变成从内存地址 0x124 开始的数组,并且可以像使用其他数组一样使用它。在我们的例子中,我们想要重新排序图像的 RGBA 缓冲区,以便实现图像旋转。要移动一个像素,我们需要每次移动 4 个连续字节(每个通道一个字节:R、G、B 和 A)。为此,我们创建了一个无符号的 32 位整数数组。按照惯例,我们的输入图像将从地址 4 开始,输出图像从输入图像结束位置开始:


int bpp = 4;int imageSize = inputWidth * inputHeight * bpp;uint32_t* inBuffer = (uint32_t*) 4;uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);
for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) { for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) { int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier)); outBuffer[i] = inBuffer[in_idx]; i += 1; }}
复制代码


在将整个 JavaScript 函数移植到 C 语言后,可以使用 emcc 编译 C 文件:


$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c
复制代码


与往常一样,Emscripten 会生成一个叫作 c.js 的胶水代码文件和一个叫作 c.wasm 的 wasm 模块。请注意,wasm 模块被压缩后只有 260 字节左右,而胶水代码在压缩大约是 3.5KB。经过一些调整之后,我们可以去掉胶水代码,并使用普通 API 来实例化 WebAssembly 模块。

Rust

Rust 是一门全新的现代编程语言,它提供了丰富的类型系统,没有运行时和所有权模型,可确保内存安全性和线程安全性。Rust 还将 WebAssembly 视为一等公民,而且 Rust 团队还为 WebAssembly 生态系统贡献了很多优秀的工具。


其中一个工具是由 rustwasm 工作组开发的 wasm-pack(https://rustwasm.github.io/wasm-pack/)。wasm-pack 可以将你的代码转换为一个对 Web 友好的模块,支持 webpack 等捆绑器,但目前仅适用于 Rust。这个工作小组正在考虑增加对其他语言的支持。


Rust 中的切片相当于 C 语言中的数组。就像在 C 语言中一样,我们需要创建切片。这违反了 Rust 的内存安全模型,因此我们必须使用 unsafe 关键字来编写不遵循内存安全模型的代码。


let imageSize = (inputWidth * inputHeight) as usize;let inBuffer: &mut [u32];let outBuffer: &mut [u32];unsafe {  inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);  outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);}
for d2 in 0..d2Limit { for d1 in 0..d1Limit { let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier; outBuffer[i as usize] = inBuffer[in_idx as usize]; i += 1; }}
复制代码


编译 Rust 文件:


$ wasm-pack build
复制代码


这个命令将产生一个 7.6KB 的 wasm 模块,以及大约 100 个字节的胶水代码(压缩之后)。

AssemblyScript

AssemblyScript 是一个相当年轻的项目,用于将 TypeScript 编译成 WebAssembly。AssemblyScript 使用与 TypeScript 相同的语法,但使用了自己的标准库。它们的标准库模拟了 WebAssembly 的功能。这意味你无法将任意 TypeScript 代码编译成 WebAssembly,但确实意味着你不必为了编写 WebAssembly 而去学习新的编程语言!


for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {  for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {    let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));    store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));    i += 1;  }}
复制代码


因为 rotate()函数具有较小的类型表面,可以很容易将其移植到 AssemblyScript。AssemblyScript 提供了用于访问原始内存的函数load<T>(ptr: usize)store<T>(ptr: usize, value: T)。要编译我们的AssemblyScript文件,只需要安装 AssemblyScript/assemblyscript 包,并运行:


$ asc rotate.ts -b assemblyscript.wasm --validate -O3
复制代码


AssemblyScript 将生成约 300 字节的 wasm 模块,并且没有胶水代码。这个模块可以与 WebAssembly API 一起使用。

瘦身

与其他两种语言相比,Rust 的 7.6KB 显得非常大。WebAssembly 生态系统中有一些工具可以用来分析 WebAssembly 文件,可以告诉你发生了什么,并帮你改善这种情况。

twiggy

twiggy是 Rust 团队开发的另一个工具,可以从 WebAssembly 模块中提取大量有用的信息。这个工具并不是特定于 Rust 的,可用来检查模块调用图、找出未使用或多余的部分,以及哪些部分占用了模块的文件大小。可以使用 twiggy 的 top 命令来查看模块文件的组成:


$ twiggy top rotate_bg.wasm
复制代码



我们可以看到,大部分文件大小来自分配器。这个有点让我们感到惊讶,因为我们的代码没有使用动态分配功能。另一个占用较大体积的是“函数名”。

wasm-strip

wasm-strip 是来自WebAssembly Binary Toolkit,简称为 wabt 的一个工具。它提供了一些工具,可用于检查和操作 WebAssembly 模块。wasm2wat 是一个反汇编程序,可以将二进制 wasm 模块转换为人类可读的格式。wabt 还包含了 wat2wasm,可以将人类可读的格式转换回二进制 wasm 模块。我们确实有使用这两个工具来检查 WebAssembly 文件,不过我们发现 wasm-strip 是最有用的。wasm-strip 从 WebAssembly 模块中移除了不必要的部分和元数据:


$ wasm-strip rotate_bg.wasm
复制代码


这样就可以将 Rust 模块文件大小从 7.5KB 减小到 6.6KB(在压缩之后)。

wasm-opt

wasm-opt 是来自Binaryen的一个工具。它尝试基于字节码对 WebAssembly 模块进行大小和性能方面的优化。Emscripten 已经在使用这个工具,有些编译器则没有。使用这些工具来节省一些额外的字节是个好主意。


wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm
复制代码


通过使用 wasm-opt,我们可以减少另外一些字节,在压缩之后只有 6.2KB。

#![no_std]

经过一些咨询和研究,我们使用#![no_std]来重新编写 Rust 代码,这样就可以不使用 Rust 的标准库。这样就可以完全禁用动态内存分配,从而从模块中删除了分配器代码。使用以下命令编译 Rust 文件:


$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs
复制代码


在使用了 wasm-opt 和 wasm-strip 之后,压缩的 wasm 模块只剩下 1.6KB。虽然它仍然比 C 语言编译器和 AssemblyScript 生成的模块大,但也足以称得上是一个轻量级的模块。

性能

除了文件大小,我们还需要优化性能。那么我们应该如何衡量性能?它们的结果又是怎样的呢?

如何进行基准测试

尽管 WebAssembly 是一种低级的字节码格式,但仍需要通过编译器生成特定于主机的机器码。就像 JavaScript 一样,编译器包含了多个阶段的工作。简单地说:第一阶段编译速度较快,但生成的代码运行速度较慢。在模块开始运行后,浏览器就会观察哪些部分是经常使用的,并通过一个更优化但速度更慢的编译器发送这些部分。


我们的用例很有趣,旋转图像的代码可能会被使用一次,或者两次。因此,在绝大多数情况下,我们无法从优化编译器中获得好处。在进行基准测试时要记住这一点。循环运行 WebAssembly 模块 10,000 次会产生不真实的结果。为了获得更真实的数字,我们应该只运行一次模块,并根据单次运行的结果做出判断。

性能比较



这两张图是相同数据的不同视图。在第一张图中,我们根据浏览器来比较,在第二张图中,我们根据使用的语言来比较。请注意,我使用了对数时间尺度,而且所有基准测试使用了相同的 1600 万像素的测试图像和相同的主机。


从图中可以看出,我们解决了原始性能问题:所有 WebAssembly 模块的运行时间都在大约 500 毫秒或更短的时间内。这证实了我们在开始时的假设:WebAssembly 为我们提供了可预测的性能。无论我们选择哪种语言,浏览器和语言之间的差异都很小。确切地说:JavaScript 的跨浏览器标准偏差约为 400 毫秒,而 WebAssembly 模块的跨浏览器标准偏差约为 80 毫秒。

工作量

另一个度量指标是创建 WebAssembly 模块并将其集成到 squoosh 的工作量。我们很难使用准确的数值来表示工作量,所以我不会创建任何图表,不过我想指出一些东西:


AssemblyScript 不仅让我们可以使用 TypeScript 来编写 WebAssembly,进行代码评审也非常容易,而且还可以生成非常小且具有良好性能的无胶水 WebAssembly 模块。


Rust 与 wasm-pack 结合使用也非常方便,但在大型的 WebAssembly 项目(需要用到绑定和内存管理)中表现更好。我们必须付出额外的工作量才能获得有竞争力的文件大小。


C 语言和 Emscripten 可以生成非常小巧且高性能的 WebAssembly 模块,但是如果没有勇气直接使用胶水代码并将其缩减到最基本的需求,那么总体大小(WebAssembly 模块+胶水代码)就会变得非常大。

结论

那么,如果你有一个 JS 热路径并希望让它运行得更快或更者像 WebAssembly 那样保持稳定的性能,你应该使用什么语言?答案是:取决于具体情况。那么我们发布的是哪个?



在比较了不同语言的模块大小/性能权衡之后,我们发现最好的选择似乎是 C 语言或 AssemblyScript。但我们最后决定发布Rust版本。我们做出这个决定基于多个原因:到目前为止,squoosh 的所有 codec 都是使用 Emscripten 编译的。我们希望能够扩展我们对 WebAssembly 生态系统的了解,并在生产环境中使用不同的语言。AssemblyScript 是一个很好的选择,但它相对年轻,编译器不像 Rust 编译器那样成熟。


虽然 Rust 和其他语言大小之间的文件大小差异在散点图中看起来非常明显,但在现实中的差异并没有那么大:即使是在 2G 网络上加载 500B 或 1.6KB 也只需要不到 1/10 秒。而且 Rust 很快就会在模块尺寸方面缩小差距。


在运行时性能方面,Rust 在浏览器中的平均速度比 AssemblyScript 快。特别是在大型项目中,Rust 更有可能在无需手动优化的情况下生成更快的代码。


AssemblyScript 允许 Web 开发人员在无需学习新语言的情况下生成 WebAssembly 模块。AssemblyScript 团队在非常积极地改进他们的工具链。我们会持续关注 AssemblyScript。


英文原文:https://developers.google.com/web/updates/2019/02/hotpath-with-wasm


2019-04-23 06:008403
用户头像

发布了 731 篇内容, 共 434.0 次阅读, 收获喜欢 1997 次。

关注

评论

发布
暂无评论
发现更多内容

并发编程-FutureTask解析 | 京东物流技术团队

京东科技开发者

并发编程 源码剖析 FutureTask 企业号 7 月 PK 榜

暑假提升休闲两不误,与华为阅读一起开启高质量听书

最新动态

用友发布业界首个企业服务大模型YonGPT

用友BIP

长连接:ChatGPT流式响应背后的逻辑 | 京东物流技术团队

京东科技开发者

websocket 长连接 企业号 7 月 PK 榜 sse

Inpaint Anything:一键进行多种图像修补

华为云开发者联盟

人工智能 华为云 华为云开发者联盟 企业号 7 月 PK 榜

从税务管理的数智化转型之路中我们能看到什么?

用友BIP

税务管理

大型企业数智化转型,工程化体系建设至关重要

用友BIP

数智底座

大文件传输过程中的网络拥塞控制方法研究

镭速

大文件传输 网络拥塞问题

再获权威认可!MIAOYUN荣获中国信通院一云多芯优秀案例,荣登《云管理产品与服务图谱》

MIAOYUN

中国信通院 一云多芯解决方案 一云多芯 可信云大会 云管理产品与服务图谱

AREX:携程新一代自动化回归测试工具的设计与实现

AREX 中文社区

开源 测试工具 回归测试 流量回放

Cloud Kernel SIG 月度动态:支持龙芯和申威架构,合入两个内存新特性

OpenAnolis小助手

开源 架构 内存 内核 龙蜥sig

Java Web应用开发案例|使用AJAX实现省市区三级联动效果

TiAmo

Java Java web 开发实例

一文让你知道等保测评和渗透测试的区别与联系

行云管家

信息安全 渗透测试 等级保护 等保测评

在langchain中使用带简短知识内容的prompt template

程序那些事

人工智能 AI 程序那些事 AI大语言模型 大语言模型

暑期读书指南 | 用缤纷字体读精品好书,华为阅读上新啦!

最新动态

【观察】智能运维的“下半场”,看云智慧如何“开新局”

云智慧AIOps社区

算法 运维 智能运维 大模型 IT运维

HTML5智慧景区三维可视化管理平台

2D3D前端可视化开发

智慧景区 智慧旅游 景区三维可视化 数字景区 智慧景区系统

初探webAssembly | 京东物流技术团队

京东科技开发者

前端 webassembly JavaScrip Blazor WebAssembly 企业号 7 月 PK 榜

英特尔合作埃森哲推出一套共计34个开源AI参考套件

E科讯

资源成本降低70%!华为MetaERP资产核算的Serverless架构实践

华为云开发者联盟

云计算 后端 华为云 华为云开发者联盟 企业号 7 月 PK 榜

JAVA和JVM运行原理是什么?

java易二三

Java 编程 JVM 计算机 程序猿

数仓现网案例丨超大结果集接收异常

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 7 月 PK 榜

HBase Compaction 原理与线上调优实践

vivo互联网技术

HBase 调优参数 Minor Compaction Compaction策略 Major Compaction

前端程序员入门:先学Vue3还是Vue2?

互联网工科生

vue.js Vue 前端

MySQL 的解析器以及 MySQL8.0 做出的改进 | StoneDB技术分享 #2

StoneDB

MySQL 数据库 HTAP StoneDB

Java一维数组是什么,怎么用?

java易二三

Java 编程 程序员 数组 计算机

澜舟科技CEO周明:不过度追求AGI,更看重大模型语言理解能力和应用落地性 | 1号位

澜舟孟子开源社区

印刷行业MES系统解决方案

万界星空科技

开源 MES系统 印刷

用Rust生成Ant-Design Table Columns | 京东云技术团队

京东科技开发者

rust swagger 企业号 7 月 PK 榜 Columns

伙伴云「页面」上线!网站、博客、资源库、文档、周报,拖拽即刻实现

联营汇聚

架构训练营模块一作业

Kleven

架构实战营

如何使用WebAssembly提升性能_大前端_Surma_InfoQ精选文章