作者|Robert Aboukhalil译者|薛命灯WebAssembly 是除 JavaScript 之外另一门可以在浏览器上运行的语言,其他语言(如 C/C++/Rust)也可以被编译成 WebAssembly 在浏览器上运行。WebAssembly 是静态类型的语言,使用线性内存,并保存成紧凑的二进制格式,所以速度非常快,可以以“接近原生”的速度运行代码(与从本地命令行运行程序的速度相当)。
到目前为止,WebAssembly 已经被用在各种应用程序中,从游戏(如 Doom 3)到将桌面应用程序移植到 Web(如 Autocad 和 Figma)。它甚至也被用到了浏览器之外,例如被作为一门高效而灵活的 Serverless 计算编程语言。
这篇文章将介绍如何使用 WebAssembly 来加速一款 Web 数据分析工具。
背景介绍
这个 Web 工具就是 fastq.bio,它是一个交互式的 Web 工具,科学家用它来快速预览 DNA 序列数据的质量。下面是这个工具的一个截图:
我不打算深入介绍这个计算过程,但总的来说,上面的图表为科学家提供了一个有关 DNA 序列质量的信息,可以帮助他们快速发现数据质量问题。
虽然现在也有很多命令行工具可用来生成这类报告图表,但 fastq.bio 的目标是让用户可以在浏览器中直接通过交互式的方式预览数据质量,这对于不习惯使用命令行的科学家来说非常有用。
这个工具的输入是一个普通文本文件,其中包含了使用 DNA 序列指令生成的 DNA 序列和其中每个核苷酸的质量分数。这种格式被称为“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 之间的数据块可以让应用程序看起来是连续的,而且可以更快地为用户返回信息,但这个数字也取决于应用程序的具体细节以及计算机的处理速度。
初始架构非常简单:
红色方框部分就是我们要进行的字符串操作,用来生成指标。这个部分是计算密集型的,所以很适合使用 WebAssembly 来优化。
WebAssembly 实现
为了搞清楚 WebAssembly 是否可以加快 Web 应用程序的速度,我们尝试了一些现成的工具,这些工具是使用 C/C++/Rust 开发的,这样就可以把它们移植成 WebAssembly,并且这些工具已经得到科学社区的认可。
经过一些调研,我们最终决定使用 seqtk(https://github.com/lh3/seqtk),这是一个被广泛使用的开源工具,使用 C 语言开发,可以用来评估序列数据的质量。
在将 seqtk 编译成 WebAssembly 之前,我们先来看看如何从源代码编译 seqtk,并在命令行中运行它。
# Compile to binary$ gcc seqtk.c \ -o seqtk \ -O2 \ -lm \ -lz
另一方面,我们可以使用 Emscripten 工具链将 seqtk 编译成 WebAssembly:
https://emscripten.org/
如果你还没有安装 Emscripten,可以从 Dockerhub 上下载我们提供的 docker 镜像,其中就包含了这个工具链:
https://hub.docker.com/r/robertaboukhalil/emsdk/tags
或者你也可以从头开始安装,只是这样需要更长的时间:
https://emscripten.org/docs/getting_started/downloads.html
$ 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 文件,而不是生成 seqtk 二进制文件。
我们使用了 USE_ZLIB 标记,这样就可以支持 zlib 库。因为 zlib 已经被移植到 WebAssembly,并被广泛使用,所以 Emscripten 将会将其包含在项目中。
我们启用了 Emscripten 的虚拟文件系统(POSIX 风格的文件系统),只是它是运行在浏览器的内存中,在页面被刷新时会消失,除非你使用 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 架构图:
如图所示,我们并没有在浏览器主线程上运行计算,而是使用了 WebWorker,这样就可以在后台线程上运行计算,避免给浏览器造成阻塞。WebWorker 的控制器负责启动 Worker,并管理与主线程之间的通信。
然后,我们让 Worker 运行 seqtk 命令来处理事先挂载好的文件。在 seqtk 运行完成之后,Worker 通过一个 Promise 将结果发送回主线程,主线程在接收到消息之后使用结果数据更新图表。与 JavaScript 实现一样,我们每次只处理一个数据块。
性能优化
为了评估使用 WebAssembly 是否确实为我们带来了速度上的优势,我们比较了 JavaScript 实现和 WebAssembly 实现每秒钟分别可以读取多少指标。我们忽略了生成交互式图表的时间,因为两者在这方面都使用了 JavaScript。
在什么都没做的情况下,我们已经可以看到 WebAssembly 比 JavaScript 有 9 倍左右的速度提升:
这个结果已经很好了,不过,我们发现,seqtk 生成了很多有用的 QC 指标,但其中有很多并没有被用到。在移除了这些没有被用到的指标之后,速度提升达到了 13 倍。
最后还有一个可改进的地方。到目前为止,fastq.bio 是通过调用两个不同的 C 函数来获取指标,其中每个函数负责计算一系列不同的指标。其中一个函数以直方图的形式返回信息,另一个则以 DNA 序列位置函数的形式返回信息。这意味着同一个数据块会被读取两次,而这其实是不必要的。
所以,我们将这两个函数的代码合成一个。因为原本的两个输出包含了不同数量的列,所以我们使用 JavaScript 来区分它们。但这样做是值得的:速度提升了 20 多倍!
注意事项
不要指望 WebAssembly 总能为我们带来 20 多倍的速度提升,有时候可能只能获得 2 倍甚至是 20% 的提升。而如果在内存中加载了太多的数据,有可能速度还会变慢,或者需要在 WebAssembly 和 JavaScript 之间进行很多的通信。