今年夏天,我参与了一个项目,旨在开发使BioJulia运行更快的工具。最终成果包括:Automa.jl现在生成更高效的代码来解析文本文件,ConcurrentCalls.jl可以并行运行多个任务,并且在TranscodingStreams.jl中提供了对各种压缩格式的简单高效的接口。
指令级并行是一种在CPU核心上同时运行多个指令的技术。这听起来可能很奇怪,但实际上这是可能的,因为CPU指令是分多个阶段执行的,例如取指、解码、执行等等。这称为指令流水线,现代处理器可以并行执行这些阶段,因此通过利用它可以提高处理吞吐量。
我在Automa.jl中实现了自动循环展开,这是一个用于根据正则表达式生成有限状态机(FSM)的包。我们用它来为生物信息学中使用的各种文件格式生成高性能解析器。Automa.CodeGenContext
的参数可以指定循环展开因子。例如,你可以这样指定值为8:context = Automa.CodeGenContext(loopunroll=8)
,就这么简单。Automa.jl的代码生成器会自动展开在FSM中找到的循环。
Automa.jl的工作原理是逐步增加读取位置变量,并从缓冲区逐字节读取数据。这很难展开,因为位置变量创建了数据依赖关系的关键路径。因此,Automa.jl展开FSM中具有自环且在状态转换中没有动作的特定节点。
让我们看看性能的提升。FASTA和FASTQ是最常用的存储生物序列的文件格式之一。我在这两种格式上对解析吞吐量进行了基准测试。随着循环展开因子的增加,吞吐量得到改善,并在因子约为10时达到饱和。
展开后的解析在两种情况下都实现了大约1.3倍的加速。此基准测试不包括I/O操作,但包括将字节数据转换为我们数据类型所需的其他操作。改进可能看起来并不令人惊讶,但是,我们应该注意,它是几乎免费获得的。无需使用更多CPU核心。
ConcurrentCalls.jl是一个使函数调用(或任务)并发运行的包。它导出一个宏@cc
,该宏将Julia代码转换为在远程进程上调用函数的代码。这是一个并行运行任务的概念示例
addprocs(2)
using ConcurrentCalls
function multitask()
# Define some time-consuming (say >10ms) task(s).
function sum(x, y)
sleep(2)
return x + y
end
function prod(x, y)
sleep(1)
return x * y
end
# Call functions concurrently using multiple processes.
@cc begin
x = sum(1, 2) # => 3
y = sum(3, 4) # => 7
prod(x, sum(y, 1)) # => 24
end
end
multitask() # => 24
在上面的示例中,x = sum(1, 2)
和y = sum(3, 4)
将在不同进程上并行执行。这些函数调用由ConcurrentCalls.jl的调度程序安排,每个任务根据一些启发式算法分配给一个工作进程。@cc
宏内部的第三行有两个函数调用。它们中的每一个也将分配给一个工作进程,但只有在前面两个完成后才会开始。任务的依赖关系由调度程序处理,并且在所有参数都完成且结果可用之前,任务都不会执行。
任务的执行以命令式的方式定义,因此其语义将从代码中显而易见。数据依赖关系通过传递给函数的参数自然地表达出来。下面的代码显示了三个使用for循环来描述作业的示例
# independent tasks
@cc begin
for i in 1:100
task()
end
end
# sequential taks (not parallelizable)
@cc begin
a = task1()
for i in 1:100
a = task2(a)
end
end
# biparallel tasks
@cc begin
a = task1()
b = task2()
for i in 1:100
a = task3(a)
b = task4(b)
end
end
该包仍处于开发的非常早期的阶段,但可以并行执行Julia代码的一些简单子集。有关更多示例和一些注意事项,请参阅ConcurrentCalls.jl的README页面。
生物信息学中并行化数据处理的一个难点在于输入文件通常是压缩的。最常见的压缩格式gzip既不支持并行解压缩,也不支持将数据拆分为较小的块。一些特殊的文件格式(例如BGZF)可以支持并行解压缩和分块,但gzip仍然很慢。
一种称为Zstandard(或Zstd)的最新压缩算法比gzip快得多,同时保持与gzip相同的压缩率。此外,它开始实现对可寻址压缩格式的实验性支持,其中数据被分成帧,每个帧独立压缩。指向特定位置的表存储在可跳过的帧中,因此它可以支持并行处理和分块。
为了支持此功能,我实现了一个Zstd的接口包,它已经作为CodecZstd.jl提供。虽然它尚不支持可寻址格式,但我们可以立即享受Zstd的性能。此外,该包构建在TranscodingStreams.jl之上,后者为许多数据格式提供了简单一致的API。目前,支持gzip(zlib)、bzip2、xz和zstd。这些包是我维护的Libz.jl和BufferedStreams.jl的替代品。
加速程序永无止境。我仍然有一些挂起的pull请求需要与这些工具配合使用才能正常工作(https://github.com/BioJulia/BioCore.jl/pull/7、https://github.com/BioJulia/BioAlignments.jl/pull/4、https://github.com/BioJulia/BioSequences.jl/pull/25)。ConcurrentCalls.jl缺乏示例、测试、基准测试以及许多在实际工作中使用的功能。CodecZstd.jl一旦稳定就会支持可寻址格式。我非常欢迎对这些设计和想法的反馈,以便进一步改进它们。
感谢我的导师Shashi Gowda以及Julia社区的其他成员的贡献。