我们如何使用 WebAssembly 将我们的 Web 应用程序加速 20 倍(案例研究)
已发表: 2022-03-10如果您还没有听说过,这里是 TL;DR:WebAssembly 是一种在浏览器中与 JavaScript 一起运行的新语言。 是的,这是对的。 JavaScript 不再是在浏览器中运行的唯一语言!
但除了“不是 JavaScript”之外,它的区别在于您可以将代码从 C/C++/Rust(以及更多! )等语言编译为 WebAssembly 并在浏览器中运行它们。 因为 WebAssembly 是静态类型的,使用线性内存,并且以紧凑的二进制格式存储,所以它也非常快,最终可以让我们以“接近原生”的速度运行代码,即接近你的速度。 d 通过在命令行上运行二进制文件来获取。 利用现有工具和库在浏览器中使用的能力以及相关的加速潜力是 WebAssembly 对 Web 如此引人注目的两个原因。
到目前为止,WebAssembly 已用于各种应用程序,从游戏(例如 Doom 3)到将桌面应用程序移植到 Web(例如 Autocad 和 Figma)。 它甚至可以在浏览器之外使用,例如作为一种高效灵活的无服务器计算语言。
本文是一个使用 WebAssembly 加速数据分析 Web 工具的案例研究。 为此,我们将使用一个用 C 编写的现有工具来执行相同的计算,将其编译为 WebAssembly,并用它来替换慢速 JavaScript 计算。
注意:本文深入探讨了一些高级主题,例如编译 C 代码,但如果您没有这方面的经验,请不要担心; 您仍然可以跟随并了解 WebAssembly 的可能性。
背景
我们将使用的网络应用程序是 fastq.bio,这是一个交互式网络工具,可以让科学家快速预览其 DNA 测序数据的质量; 测序是我们读取 DNA 样本中“字母”(即核苷酸)的过程。
这是运行中的应用程序的屏幕截图:
我们不会详细介绍计算的细节,但简而言之,上面的图表让科学家们了解测序的进展情况,并用于一目了然地识别数据质量问题。
尽管有数十种命令行工具可用于生成此类质量控制报告,但 fastq.bio 的目标是在不离开浏览器的情况下提供数据质量的交互式预览。 这对于不熟悉命令行的科学家特别有用。
应用程序的输入是由测序仪器输出的纯文本文件,其中包含 DNA 序列列表和 DNA 序列中每个核苷酸的质量分数。 该文件的格式称为“FASTQ”,因此命名为 fastq.bio。
如果您对 FASTQ 格式感到好奇(不是理解本文所必需的),请查看 FASTQ 的 Wikipedia 页面。 (警告:FASTQ 文件格式在该领域已知会引起手掌。)
fastq.bio:JavaScript 实现
在 fastq.bio 的原始版本中,用户首先从他们的计算机中选择一个 FASTQ 文件。 使用File
对象,应用程序从随机字节位置开始读取一小块数据(使用 FileReader API)。 在该数据块中,我们使用 JavaScript 执行基本的字符串操作并计算相关指标。 一个这样的指标可以帮助我们跟踪我们通常在 DNA 片段的每个位置看到多少个 A、C、G 和 T。
一旦为该数据块计算了指标,我们就可以使用 Plotly.js 以交互方式绘制结果,然后转到文件中的下一个块。 以小块处理文件的原因仅仅是为了改善用户体验:一次处理整个文件会花费太长时间,因为 FASTQ 文件通常在数百 GB 大小。 我们发现 0.5 MB 到 1 MB 之间的块大小会使应用程序更加无缝,并且会更快地将信息返回给用户,但是这个数字会根据应用程序的详细信息和计算量的大小而有所不同。
我们最初的 JavaScript 实现的架构相当简单:
红色框是我们进行字符串操作以生成指标的地方。 该框是应用程序中计算密集度更高的部分,这自然使其成为使用 WebAssembly 进行运行时优化的良好候选者。
fastq.bio:WebAssembly 实现
为了探索我们是否可以利用 WebAssembly 来加速我们的 Web 应用程序,我们搜索了一个现成的工具来计算 FASTQ 文件的 QC 指标。 具体来说,我们寻求一种用 C/C++/Rust 编写的工具,以便它能够移植到 WebAssembly,并且已经得到科学界的验证和信任。
经过一些研究,我们决定使用 seqtk,这是一个用 C 编写的常用开源工具,可以帮助我们评估测序数据的质量(并且更普遍地用于操作这些数据文件)。
在我们编译到 WebAssembly 之前,让我们首先考虑一下我们通常如何将 seqtk 编译为二进制文件以在命令行上运行它。 根据 Makefile,这是您需要的gcc
咒语:
# Compile to binary $ gcc seqtk.c \ -o seqtk \ -O2 \ -lm \ -lz
另一方面,要将 seqtk 编译为 WebAssembly,我们可以使用 Emscripten 工具链,它为现有的构建工具提供了替代品,使在 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 之间的差异很小:
- 我们要求 Emscripten 生成一个
.wasm
和一个.js
来处理我们的 WebAssembly 模块的实例化,而不是输出二进制文件seqtk
- 为了支持 zlib 库,我们使用标志
USE_ZLIB
; zlib 非常常见,以至于它已经被移植到 WebAssembly,Emscripten 会为我们将它包含在我们的项目中 - 我们启用 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 架构:
如图所示,我们不是在浏览器的主线程中运行计算,而是使用 WebWorkers,它允许我们在后台线程中运行计算,避免对浏览器的响应产生负面影响。 具体来说,WebWorker 控制器启动 Worker 并管理与主线程的通信。 在 Worker 端,API 执行它收到的请求。
然后我们可以让 Worker 对我们刚刚挂载的文件运行 seqtk 命令。 当 seqtk 完成运行时,Worker 通过 Promise 将结果发送回主线程。 一旦收到消息,主线程就会使用结果输出来更新图表。 与 JavaScript 版本类似,我们分块处理文件并在每次迭代时更新可视化。
性能优化
为了评估使用 WebAssembly 是否有任何好处,我们使用每秒可以处理多少读取的指标来比较 JavaScript 和 WebAssembly 的实现。 我们忽略了生成交互式图形所花费的时间,因为这两种实现都使用 JavaScript 来达到这个目的。
开箱即用,我们已经看到了约 9 倍的加速:
这已经很好了,因为它相对容易实现(那就是一旦你了解了 WebAssembly!)。
接下来,我们注意到虽然 seqtk 输出了许多通常有用的 QC 指标,但其中许多指标并没有被我们的应用程序实际使用或绘制成图表。 通过删除一些我们不需要的指标的输出,我们能够看到 13 倍的更大加速:
考虑到它是多么容易实现,这又是一个很大的改进——通过逐字注释掉不需要的 printf 语句。
最后,我们还研究了一项改进。 到目前为止,fastq.bio 获取感兴趣指标的方式是调用两个不同的 C 函数,每个函数计算一组不同的指标。 具体来说,一个函数以直方图的形式返回信息(即,我们将值分类为范围的列表),而另一个函数返回作为 DNA 序列位置函数的信息。 不幸的是,这意味着同一个文件块被读取了两次,这是不必要的。
因此,我们将这两个函数的代码合并为一个函数(尽管有些杂乱)(甚至不必复习我的 C 语言!)。 由于两个输出具有不同的列数,我们在 JavaScript 方面进行了一些争论以解开两者。 但这是值得的:这样做让我们实现了 20 倍以上的加速!
小心的话
现在是提出警告的好时机。 当你使用 WebAssembly 时,不要期望总能获得 20 倍的加速。 您可能只能获得 2 倍或 20% 的加速。 或者,如果您在内存中加载非常大的文件,或者需要在 WebAssembly 和 JavaScript 之间进行大量通信,您可能会变慢。
结论
简而言之,我们已经看到,用调用已编译的 WebAssembly 来替换慢速 JavaScript 计算可以显着提高速度。 由于这些计算所需的代码已经存在于 C 中,因此我们获得了重用可信工具的额外好处。 正如我们还提到的,WebAssembly 并不总是适合这项工作的工具(喘气! ),所以要明智地使用它。
延伸阅读
- “使用 WebAssembly 升级”,Robert Aboukhalil
构建 WebAssembly 应用程序的实用指南。 - 蒜泥蛋黄酱(在 GitHub 上)
用于构建快速基因组学网络工具的框架。 - fastq.bio 源代码(在 GitHub 上)
用于 DNA 测序数据质量控制的交互式网络工具。 - “WebAssembly 的简短卡通介绍”,Lin Clark