使用 GNU 并行基于行的输出,无需临时文件

使用 GNU 并行基于行的输出,无需临时文件

GNU并行的默认输出模式是--group
每个作业的输出都写入临时文件,并parallel仅在作业完成后才传递到输出。

当对大于/tmp空间的数据使用此默认输出模式时,
parallel lz4 -dc ::: /var/lib/apt/lists/*lz4 | wc 它会很慢并且会崩溃
parallel: Error: Output is incomplete.
Cannot append to buffer file in /tmp.

使用该模式时--ungroup,行在中间分开,这会导致与
parallel --ungroup lz4 -dc ::: /var/lib/apt/lists/*lz4 | wc
unparallelized 的输出不同lz4 -dmc /var/lib/apt/lists/*lz4 | wc

根据parallel手册页,这应该通过我理解的选项来解决 --line-buffer:所有作业都有一个由并行读取的输出管道,如果任何作业的输出可用,它会逐行传递到并行的输出管道过程本身。 (编辑:我的意思是块中的行,就像将大量输入分散到并行进程一样,而不是每行一个系统调用,这会太慢。)

但这不起作用:
parallel --line-buffer lz4 -dc ::: /var/lib/apt/lists/*lz4 | wc -c
导致与--group上面暗示的相同的磁盘已满错误。

parallel --line-buffer没有临时文件如何使用?

系统是LUbuntu 20 LTS。parallel -V返回20161222。具有超线程(4 线程)的双核 i3-4130 上原始串行和并行解压性能的比较:

time ls -S /var/lib/apt/lists/*lz4 | parallel --ungroup lz4 -dc > /dev/null
1.461s
time lz4 -dmc /var/lib/apt/lists/*lz4 > /dev/null
3.069s

真实用例如下(不带 的解决方法--line-buffer):

time lz4 -dmc /var/lib/apt/lists/*Contents* | grep -F $'/parallel\t' | sort -u
usr/bin/parallel                                            universe/utils/moreutils,universe/utils/parallel
usr/bin/parallel                                            universe/utils/parallel
usr/lib/R/library/parallel/R/parallel                       universe/math/r-base-core
usr/lib/cups/backend/parallel                               net/cups-filters
usr/share/doc-base/parallel                                 universe/utils/parallel
real    0m5.349s
user    0m3.970s
sys     0m5.839s

time ls -S /var/lib/apt/lists/*Contents* | parallel lz4 -dc '{}' \| grep -F "\$'/parallel\t'" | sort -u
(same output as above)
real    0m3.669s
user    0m5.888s
sys     0m7.676s

这不仅可以并行化解压,还可以并行化后处理,并且是更好的解决方案,因为管道第一部分的工作不是 99%。
但是这种并行化整个管道的方法并不总是可行,因此对于第一步的输出不是很小并且需要流式传输的情况,一般问题仍然存在。

答案1

要执行您的建议,需要lz4将每个管道的输出发送到单独的管道,并且需要一个从所有管道读取并将其输出拆分为几行的选择/轮询循环,或者让一个进程/线程处理每个管道。

这听起来像是一笔巨大的开销。即使没有这种开销,我发现在我的 12 年历史的 4 核 8 线程笔记本电脑上,带有快速 SSD printf '%s\0' /var/lib/apt/lists/*lz4 | xargs -r0 -n 1 -P8 lz4 -dc(即使没有 GNU 并行开销)也不会比lz4 -dmc /var/lib/apt/lists/*lz4.

理想情况下,您首先希望并行命令输出行缓冲。对于很多,你可以用 来做到这一点stdbuf -oL

但情况似乎并非如此lz4,但您可以通过以下方式手动实现第二种方法(一个进程处理一个lz4输出):

printf '%s\0' /var/lib/apt/lists/*lz4 |
  stdbuf -oL xargs -r0 -n 1 -P4 sh -c 'lz4 -dc "$1" | paste' sh | 
  wc -c

paste这是一个一次处理一行输入的命令,并且可以确信对输出进行行缓冲stdbuf,另请参见 GNUgrepgrep --line-buffered '^';避免sed -u一次输出一行,但一次读取一个字节的输入)。

即使将输出丢弃到/dev/null,在我的系统上,速度也比非并行系统慢 13 倍lz4 -dmc /var/lib/apt/lists/*lz4(6.5 秒 vs 0.5 秒)。

这是使用paste用 C 编写的。GNU 并行是用 编写的perl,如果内部确实支持类似的东西,那么很有可能它的效率会更低。

并行化(至少以这种方式)仅对产生相对较少文本输出的 CPU 密集型任务有意义,这lz4与轻松解压缩相反。

答案2

20170822 版本的发行说明中写道:

  • --line-buffer 不再使用临时文件。这速度更快,并且使得单个进程可以输出比可用磁盘空间更多的数据。

所以解决办法就是升级到20170822。

GNU Parallel 仍将临时文件用于其他用途,但不用于行缓冲。

如果您排长队,它也会做正确的事情:

#!/bin/bash

5gfile() {
    # Create file with 5GB long line
    perl -e '$a=(shift)x1000000;for(1..5000){print $a};print "\n"' $1 | lz4 > $1.lz4;
}
export -f 5gfile
parallel 5gfile ::: a b c d

echo The correct output: One line with a b c d
lz4 -dc {a..d}.lz4 | tr -s abcd

echo Output from parallel: One line with a b c d might be reordered
parallel --line-buffer lz4 -dc ::: {a..d}.lz4 |
    tr -s abcd

echo Output from xargs with stdbuf -oL
echo This does not handle long lines because stdbuf -oL does not guarantee only full lines will be written
printf '%s\0' /tmp/*lz4 |
    stdbuf -oL xargs -r0 -n 1 -P4 sh -c 'lz4 -dc "$1" | paste' sh |
    tr -s abcd 

相关内容