paste
是一个出色的工具,但速度非常慢:运行时我的服务器上的速度约为 50 MB/s:
paste -d, file1 file2 ... file10000 | pv >/dev/null
paste
根据 ,使用 100% CPU top
,因此它不受慢速磁盘等因素的限制。
查看源代码,可能是因为它使用了getc
:
while (chr != EOF)
{
sometodo = true;
if (chr == line_delim)
break;
xputchar (chr);
chr = getc (fileptr[i]);
err = errno;
}
是否有其他工具可以完成相同的操作,但速度更快?也许一次读取 4k-64k 块?也许通过使用向量指令并行查找换行符而不是一次查看一个字节?也许使用awk
或类似?
输入文件是 UTF8 并且太大以至于无法放入 RAM,因此读取一切进入内存不是一个选择。
编辑:
Thanasisp 建议并行运行作业。这稍微提高了吞吐量,但仍然比 pure 慢一个数量级pv
:
# Baseline
$ pv file* | head -c 10G >/dev/null
10.0GiB 0:00:11 [ 897MiB/s] [> ] 3%
# Paste all files at once
$ paste -d, file* | pv | head -c 1G >/dev/null
1.00GiB 0:00:21 [48.5MiB/s] [ <=> ]
# Paste 11% at a time in parallel, and finally paste these
$ paste -d, <(paste -d, file1*) <(paste -d, file2*) <(paste -d, file3*) \
<(paste -d, file4*) <(paste -d, file5*) <(paste -d, file6*) \
<(paste -d, file7*) <(paste -d, file8*) <(paste -d, file9*) |
pv | head -c 1G > /dev/null
1.00GiB 0:00:14 [69.2MiB/s] [ <=> ]
top
仍然表明,外部才是paste
瓶颈。
我测试了增加缓冲区是否会产生影响:
$ stdbuf -i8191 -o8191 paste -d, <(paste -d, file1?) <(paste -d, file2?) <(paste -d, file3?) <(paste -d, file4?) <(paste -d, file5?) <(paste -d, file6?) <(paste -d, file7?) <(paste -d, file8?) <(paste -d, file9?) | pv | head -c 1G > /dev/null
1.00GiB 0:00:12 [80.8MiB/s] [ <=> ]
这增加了 10% 的吞吐量。进一步增加缓冲区没有任何改善。这可能与硬件有关(即,可能是由于 1 级 CPU 高速缓存的大小所致)。
测试在 RAM 磁盘中运行以避免与磁盘子系统相关的限制。
答案1
使用不同的替代方案和场景进行了一些进一步的测试
(编辑:补充编译版本的截止值)
长话短说:
- 是的,coreutils
paste
比cat
- 似乎没有比 coreutils 更快的容易获得的替代方案
paste
,特别是对于大量短线 paste
在行长度、行数和文件数的不同组合中,吞吐量惊人地稳定- 对于更长的线路,下面提供了更快的替代方案
详细地:
我测试了很多场景。吞吐量测量是pv
按照原始帖子中的方式进行的。
比较方案:
cat
(来自 GNU coreutils 8.25 作为基准)paste
(同样来自 GNU coreutils 8.25)- python 脚本从上面回答
- 替代Python脚本(用常规循环替换用于收集行片段的列表理解)
- 尼姆计划(类似于4。但已编译为可执行文件)
文件/行号组合:
# | 列 | 线 |
---|---|---|
1 | 20万 | 1,000 |
2 | 20,000 | 10,000 |
3 | 2,000 | 100,000 |
4 | 200 | 1,000,000 |
5 | 20 | 10,000,000 |
6 | 2 | 100,000,000 |
每次测试的数据总量相同(1.3GB)。每列由 6 位数字组成(例如 000'001 到 200'000)。上述组合尽可能分布在 1、10、100、1'000 和 10'000 个同等大小的文件中。
生成的文件如下:yes {000001..200000} | head -1000 > 1
粘贴是这样完成的:for i in cat paste ./paste ./paste2 ./paste3; do $i {00001..1000} | pv > /dev/null; done
然而,粘贴的文件实际上都是指向同一个原始文件的链接,因此所有数据无论如何都应该在缓存中(在粘贴之前直接创建并首先读取cat
;系统内存为128GB,缓存大小34GB)
运行了另一组,其中数据是动态创建的,而不是从预先创建的文件中读取并通过管道传输到paste
(如下所示,文件数=0)。
对于最后一组命令就像for i in cat paste ./paste ./paste2 ./paste3; do $i <(yes {000001..200000} | head -1000) | pv > /dev/null; done
发现:
paste
慢一个数量级cat
paste
在各种线宽和涉及的文件数量范围内,其吞吐量极其一致(约 300MB/s)。- 一旦平均输入文件行长度超过一定限制(在我的测试机器上约为 1400 个字符),自制的 Python 替代方案就可以显示出一些优势。
- 与 python 脚本相比,编译的 nim 版本的吞吐量大约是 python 脚本的两倍。与此相比,
paste
一个输入文件的收支平衡点约为 500 个字符。随着输入文件数量的增加,该值会减少,一旦涉及至少 10 个输入文件,每个输入文件行就会减少到约 150 个字符。 - python 和 nim 版本都遭受许多短行的处理开销(可疑原因:在使用的两个 stdlib 函数中尝试检测行结尾并将其转换为特定于平台的结尾)。但coreutils
paste
不受影响。 - 看来,同时的动态数据生成过程是
cat
以及具有较长行的 nim 版本的限制因素,并且也在一定程度上影响了处理速度。 - 在某些时候,大量打开的文件句柄甚至对 coreutils 似乎也会产生不利影响
paste
。 (只是推测:也许这甚至会影响并行版本?)
结论(至少对于测试机)
- 如果输入文件很窄,请使用 coreutils Paste,特别是当文件很长时。
- 如果输入文件相当宽,则首选替代方案(Python 版本的输入文件行长度 > 1400 个字符,nim 版本的输入文件行长度为 150-500 个字符,具体取决于输入文件的数量)。
- 通常更喜欢编译的 nim 版本而不是 python 脚本。
- 小心太多的小碎片。在这种情况下,进程的默认软限制 1024 个打开文件似乎相当合理。
针对OP情况的建议(并行处理)
如果输入文件很窄,请paste
在内部作业中使用 coreutils 并已编译的替代方案对于最外层的进程。如果所有文件都有长行,一般使用 nim 版本。
洞穴:链接的程序是临时版本,按原样提供,没有任何保证,也没有明确的错误处理。此外,分隔符在所有三种实现中都是硬编码的。
答案2
尽管 getc 得到了高度优化,并且现在主要实现为缓冲区支持的宏,但我同意它可能仍然是瓶颈 - 正如您所怀疑的那样,或者更确切地说:所使用的缓冲区的大小可能相对较小,导致仍然很高文件读取次数。
虽然我没有得到上面显示的确切数字,但在我的测试中,基线和粘贴运行之间仍然存在明显差异。
(也许罪魁祸首是缓冲中的开关(文件的块,流的行)。但是我不是那么有经验的wtr)
进一步测试时,使用以下结构时,我的吞吐量出现了类似的下降:
cat file* | dd | pv > /dev/null
中间插入 dd 的吞吐量与粘贴运行的吞吐量非常相似。 (dd 默认情况下使用 512 字节的块大小)。如果进一步减小缓冲区大小,运行时间也会成比例增加。然而,当将块大小增加到仅几 kb(例如 8 或 16)时,速度会显着提高。当使用 1 或 2M 时,它起飞了:
cat file* | dd bs=2M | pv > /dev/null
似乎有可能更改 getc 的缓冲区大小(如果 stdout 从行缓冲切换到具有自由选择大小的块缓冲)。然而,必须记住,如果有数千个打开的文件,并且每个文件都有一个缓冲区,那么内存需求会迅速增加。
尽管如此,人们可以尝试改变缓冲(参见例如https://stackoverflow.com/questions/66219179/usage-of-getc-with-a-file)通过使用适当的 setvbuf 调用,看看会发生什么。
添加:目前我不知道有一个可比较的公开可用的程序可以更快地完成相同的任务。
(PS:刚才看到你的名字,你不是 GNU 并行专家吗?干得好!)
答案3
由于我过去经历过相当快的 python 文本处理(即高度优化和高度调整的 python 文本处理例程),所以我只是拼凑了一个小的 q&d python 脚本,并将其与 coreutils 的粘贴进行比较。 (当然,这个东西的选项非常有限,因为它仅用于演示目的(仅接受当前文件的文件名),并且列分隔符是硬连线的。)
将其放入paste
当前目录中调用的文本文件中,提供执行权限并尝试一下。
#! /usr/bin/env python3
import sys
filenames = sys.argv[1:]
infiles = [open(i,'r') for i in filenames]
while True:
lines = [i.readline() for i in infiles]
if all([i=='' for i in lines]):
break
print("\t".join([i.strip("\n") for i in lines]))
更新:修复了上述脚本中的一个错误,该错误导致在所有输入文件中发现同一行为空时该脚本立即中止。下面还更新了 python 版本的计时测量,以反映实际运行时间。
在我测试的几个系统上,预热缓存后(不费心创建 ramdisk)上面的 python 版本始终优于 coreutils Paste幅度为 10%(原生 Debian Buster)到 30%(运行 Debian Stretch 的虚拟机)。在 Windows 上,差异更加明显,但这可能是由于额外的 posix 转换开销或不同的缓存(带有 coreutils Paste 8.26 的 cygwin:>20 倍慢;带有 coreutils Paste 8.32 的 msys2:比 python 版本慢 >12 倍;两者都运行 python 3.9 .9).对于这些测试,我只是创建了一个包含 100 行很长行的文件并将其粘贴到自身,因为问题似乎在于长行的处理。
jf1@s1 MSYS /d/temp/ui
# time paste b b > /dev/null
real 0m5.920s
user 0m5.896s
sys 0m0.031s
jf1@s1 MSYS /d/temp/ui
# time ../paste b b > /dev/null
real 0m0.480s
user 0m0.295s
sys 0m0.170s
jf1@s1 MSYS /d/temp/ui
# ../paste b b > c1
jf1@s1 MSYS /d/temp/ui
# paste b b > c2
jf1@s1 MSYS /d/temp/ui
# diff c1 c2
jf1@s1 MSYS /d/temp/ui
#