阅读 373

[译] 用 WebAssembly 提速 Web App 20 倍(实例学习)

原文:How We Used WebAssembly To Speed Up Our Web App By 20X (Case Study)

作者:www.smashingmagazine.com/contact/

译文:如何使用 WebAssembly 来提高我们的 Web 应用的性能 20 倍(实例学习)

译者:Zavier Tang


概述:在这篇文章中,我们将探索如何用编译后的 WebAssembly 替代缓慢的 JavaScript 程序,从而提升 Web App 应用程序的速度。

如果你还没有听说过 WebAssembly,这是它的介绍:WebAssembly 是一种与 JavaScript 一起运行在浏览器中的一种新的语言。没错!也就是说 JavaScript 不再是唯一能在浏览器中运行的语言了!

但是,除了它与 JavaScript 的名称不同之外,它的独特之处在于,你可以从 C / C++ / Rust 等更多的语言编译成 WebAssembly,并在浏览器中运行它们。因为 WebAssembly 是静态类型的,使用线性内存,并以较小的二进制格式存储,所以它的运行速度非常快,可能接近本机原生程序的速度(即以接近于在命令行上运行二进制代码的速度运行)。在浏览器中利用现有的工具和库,以及相关的提速潜力,是 WebAssembly 如此强大的两个原因。

到目前为止,WebAssembly 已被用于各种应用程序,从游戏(如 毁灭战士 3)到将桌面应用程序移植到 Web (如 AutocadFigma。它甚至可以在浏览器之外使用,例如可以作为一种用于 serverless computing 的高效且灵活的语言。

本文是一个使用 WebAssembly 加速数据分析 Web 工具的案例。为此,我们将使用一个用 C 语言编写的可以执行相同计算的程序,将其编译为 WebAssembly,使用它来替换缓慢的 JavaScript

注意:本文深入研究了一些高级内容,比如编译 C 代码。但是如果你没有这方面的经验,请不要担心,你仍然能够一起来了解 WebAssembly 的功能。

背景介绍

我们将使用的 Web 应用程序是 fastq.bio,它是一个交互式网络工具,为科学家提供快速预览 DNA 测序数据的质量。测序是我们读取 DNA 样本中的 “字母” (即核苷酸)的过程。

下面是应用程序的截图(查看大图):

Interactive plots showing the user metrics for assessing the quality of their data

我们不会详细地讨论计算的细节,但是简单地说,上图是为科学家提供了排序进行过程的可视化交换体验,并被用于快速识别数据质量。

尽管有许多的命令行工具可以生成这样的质量控制报告,但是 fastq 的目标是,在不离开浏览器的情况下提供数据质量的交互式预览。对于不熟悉命令行的科学家来说,这非常有用。

应用程序的输入是一个纯文本文件,由测序仪输出,包含 DNA 序列列表和 DNA 序列中每个核苷酸的质量分数。该文件的格式称为 “FASTQ”,因此这个工具被叫做 fastq.bio

如果您对 “FASTQ” 格式感兴趣,请查看 Wikipedia page 了解更多。

用 JavaScript 实现 Fastq.Bio

在 fastq.bio 的原始版本中,用户首先从计算机中选择一个 FASTQ 文件。使用 File 对象,应用程序从随机的字节位置开始读取一小块数据(使用 FileReader API)。在该数据块中,我们使用 JavaScript 执行基本的字符串操作并计算相关指标。其中一个度量标准帮助我们跟踪我们通常在 DNA 片段的每个位置上看到的 A、C、G 和 T 的数量。

一旦为该数据块计算了度量标准,我们就会用 Plotly.js 交互式地绘制结果,然后继续处理文件中的下一个块。将文件分成小块处理的原因仅仅是为了改进用户体验:一次处理整个文件将花费太长的时间,因为 FASTQ 文件通常是几百 GB 的。我们发现,0.5 MB 到 1 MB 之间的块大小将使应用程序的计算更加完美,并将更快地向用户返回信息,但是这个数字大小会根据应用程序的细节和计算量的大小而变化。

我们最初的 JavaScript 实现的架构相当简单:

Randomly sample from the input file, calculate metrics using JavaScript, plot the results, and loop around

fastq.bio 的 JavaScript 实现架构。(查看大图)

红色的框是我们进行字符串操作以生成度量的地方。该框是应用程序中计算密集型的部分,这自然使它成为使用 WebAssembly 进行运行时优化的目标。

用 WebAssembly 实现 Fastq.Bio

为了探索我们是否可以利用 WebAssembly 来加速我们的 Web 应用程序,我们搜索了一个现成的工具来计算 FASTQ 文件上的 QC 指标。具体地说,我们寻找了一个用 C / C++ / Rust 编写的工具,这样它就可以移植到 WebAssembly,而且这个工具已经得到了科学界的验证和信任。

经过一些研究,我们决定使用 seqtk,这是一个用 C 语言编写的常用的开源工具,可以帮助我们评估测序数据的质量(通常用于操作这些数据文件)。

在编译到 WebAssembly 之前,让我们首先考虑如何将 seqtk 编译为二进制文件,以便在命令行上运行它。根据生成文件,你需要执行以下 gcc 命令:

# Compile to binary
$ gcc seqtk.c \
   -o seqtk \
   -O2 \
   -lm \
   -lz
复制代码

另一方面,要将 seqtk 编译到 WebAssembly,我们可以使用 Emscripten toolchain,它为现有的构建工具提供了替换,从而使得在 WebAssembly 中工作更加容易。如果你没有安装 Emscripten,你可以下载我们在 Dockerhub 上准备的 docker 镜像,里面有你需要的工具(你也可以从头开始安装,但通常需要一段时间):

$ docker pull robertaboukhalil/emsdk:1.38.26
$ docker run -dt --name wasm-seqtk robertaboukhalil/emsdk:1.38.26
复制代码

在容器内部,我们可以使用 emcc 编译器代替 gcc

# Compile to WebAssembly
$ emcc seqtk.c \
    -o seqtk.js \
    -O2 \
    -lm \
    -s USE_ZLIB=1 \
    -s FORCE_FILESYSTEM=1
复制代码

如你所见,编译为二进制和 WebAssembly 相比差异是非常小的:

  1. 我们要求 Emscripten 生成一个 .wasm.js 来处理 WebAssembly 模块的实例化,而不是输出二进制文件 seqtk
  2. 为了支持 zlib 库,我们使用 USE_ZLIB 标志;zlib 非常常见,它已经被移植到 WebAssembly,Emscripten 将在我们的项目中包含它
  3. 我们启用了 Emscripten 的虚拟文件系统,这是一个类似 POSIX 的文件系统(这里是源码),但是它在浏览器的 RAM 中运行,当你刷新页面时就会消失(除非你使用 IndexedDB 将其状态保存在浏览器中,但这应该放在另一篇文章中讨论)。

为什么是虚拟文件系统?为了回答这个问题,让我们比较一下如何在命令行上调用 seqtk 和使用 JavaScript 调用编译后的 WebAssembly 模块:

# On the command line
$ ./seqtk fqchk data.fastq

# In the browser console
> Module.callMain(["fqchk", "data.fastq"])
复制代码

访问虚拟文件系统非常强大,因为这意味着我们不必重写 seqtk 来处理字符串输入。我们可以在虚拟文件系统上挂载一组数据作为文件 data.fastq,并简单地调用 seqtk 的 main() 函数。

将 seqtk 编译到 WebAssembly 后,下面是新的 fastq.bio:

Randomly sample from the input file, calculate metrics within a WebWorker using WebAssembly, plot the results, and loop around

WebAssembly + WebWorkers 实现的 fastq.bio(查看大图

如图所示,我们没有在浏览器的主线程中运行计算,而是使用 WebWorkers,它允许我们在后台线程中运行计算,从而避免对浏览器的响应性产生负面影响。具体来说,WebWorker 控制器启动 Worker 并管理与主线程的通信。在 Wroker 的一端,使用相关 API 执行它接收到的请求。

然后,我们可以要求 Worker 对刚刚挂载的文件运行 seqtk 命令。当 seqtk 运行完毕时,Worker 通过一个 Promise 将结果发送回主线程。一旦接收到消息,主线程就将结果输出来更新图表。与 JavaScript 版本类似,我们以块的形式处理文件,并在每次迭代中更新可视化视图。

优化

为了评估使用 WebAssembly 是否有任何好处,我们使用每秒可以处理多少读取数据来比较 JavaScript 和 WebAssembly。我们忽略了生成交互式图形所花费的时间,因为这两种实现方法都使用 JavaScript 来实现此目的。

开箱即见,有大约 9 倍的加速:

Bar chart showing that we can process 9X more lines per second

使用 WebAssembly,我们可以看到与最初的 JavaScript 实现相比,速度提高了 9 倍。(查看大图

这已经非常好了,因为它相对来说比较容易实现(只需要你理解了 WebAssembly 即可)。

接下来,我们注意到,虽然 seqtk 输出了很多通常有用的 QC 指标,但我们的应用程序实际上并没有使用或绘制这些指标。通过删除一些我们不需要的指标的输出,可以看到一个更大的速度,13 倍:

Bar chart showing that we can process 13X more lines per second

删除不必要的输出可以进一步提高性能。(查看大图

这又是一个很大的改进,因为很容易实现,通过注释掉不需要的输出语句。

最后,我们研究了另一个改进。到目前为止,fastq.bio 获得相关度量的方法是通过两个不同的 C 函数,每个函数计算一组不同的度量。具体来说,一个函数以直方图的形式返回信息(即我们放入范围的值列表),而另一个函数以 DNA 序列位置的函数返回信息。不幸的是,这意味着相同的文件块被读取两次,这是不必要的。

因此,我们将这两个函数的代码合并到一个函数中(尽管有些混乱)。由于这两个输出的列数不同,所以我们在 JavaScript 端进行了一些区分,以便将它们分开。但这是值得的:这样做让我们实现了大于 20 倍的加速!

Bar chart showing that we can process 21X more lines per second

最后,使代码只读取每个文件块一次可以提高大于 20 倍的性能。(查看大图

注意

现在是提出警告的好时机。当你使用 WebAssembly 时,不要总是期望得到 20 倍的加速。你可能只得到 2 倍的加速或者 20% 的加速。如果在内存中加载非常大的文件,或者需要在 WebAssembly 和 JavaScript 之间进行大量通信,那么速度可能会变慢。

总结

简而言之,我们已经看到用调用编译后的 WebAssembly 来替代缓慢的 JavaScript 程序可以显著提高速度。由于这些计算所需的代码已经用 C 语言实现过了,所以我们获得了重用可信工具的额外好处。正如我们还提到的, WebAssembly 并不总是适合这项工作的工具,所以要明智地使用它。

延伸阅读


译者注:《JavaScript Weekly》周刊正在翻译中...

请戳 -> JavaScript-Weekly-zh-CN

关注下面的标签,发现更多相似文章
评论