Ruby输入/输出性能-逐个字符读取文件

6

简要版: 如何在Ruby中从STDIN(或文件)逐个字符地读取数据,同时保持高性能?(虽然问题可能不是特定于Ruby的)

详细版: 在学习Ruby时,我正在设计一个小工具,它必须从管道文本数据中读取数据,查找并收集其中的数字并进行一些处理。

cat huge_text_file.txt | program.rb

input  > 123123sdas234sdsd5a ...
output > 123123, 234, 5, ...

文本输入可能非常大(以GB计),可能不包含换行符或空格(任何非数字字符都是分隔符),因此我进行了逐个字符的读取(尽管我对性能有所担忧),结果发现这种方式非常慢。

仅仅对一个900KB的输入文件进行逐个字符的读取,需要大约7秒钟的时间!

while c = STDIN.read(1)
end

如果我输入包含换行符的数据并逐行读取,同一文件的读取速度会快100倍。
while s = STDIN.gets
end

似乎使用 STDIN.read(1) 从管道读取数据时不涉及任何缓冲,每次读取都会访问硬盘 - 但是操作系统难道不应该对其进行缓存吗? STDIN.gets 不是内部逐个字符读取直到遇到 '\n' 吗?
使用 C 时,我可能会以块的形式读取数据,虽然我必须处理数字被缓冲窗口分割的情况,但这看起来并不像 Ruby 的优雅解决方案。那么正确的方法是什么呢? P.S 在 Python 中计时读取相同文件:
for line in f:
    line
f.close()

运行时间为0.01秒。

c = f.read(1)
while c:
    c = f.read(1)
f.close()

运行时间为0.17秒。

谢谢!


我怀疑这里的问题不是IO,而是垃圾回收器。使用read(1)将为文件的每个字节创建一个新的字符串对象。如果您创建一个临时字符串并在每次调用read时重复使用它,您可能会获得更好的结果。因此,如果您首先执行buffer = "",则可以在循环中调用STDIN.read(1, buffer) - matt
@matt 谢谢你的建议!我试过了,速度稍微快了一点。对于从 cat 管道传输的 900K 文件,STDIN.read(1) 平均运行时间为 4.6 秒,buffer=''; STDIN.read(1,buffer) 为 4.5 秒,STDIN.gets 为 0.08 秒。我还尝试将所有内容从 SSD 移动到 HDD,看看是否有影响 - 没有。我认为输入文件必须由操作系统缓存,并且硬盘实际上并没有每次读取。 - epsylon
我在Python中计时了相同的东西,for line in file: line 的运行时间为0.01秒,而while c: c=file.read(1)则为0.17秒。尽管如此,按块读取仍然比它快10倍以上。 - epsylon
1
尝试使用each_byte代替while循环:STDIN.each_byte do |b| ... end。这种方式似乎更快,但我不太清楚原因。 - matt
(或者根据您的数据使用 each_char,需要考虑编码)。 - matt
@matt 非常感谢,确实比read(1)快10倍!如果您将其发布为答案,我会接受它。最终我还是使用了分块读取(并处理分割的数字),因为使用大缓冲区似乎是最快的方式。 - epsylon
1个回答

4
这个脚本逐个单词读取IO对象,并在发现1000个单词或到达文件结尾时执行块。
同时最多只会在内存中保存1000个单词。注意,使用" "作为分隔符意味着“单词”可能包含换行符。
该脚本使用IO#each指定分隔符(在本例中为空格,以获得Enumerator单词),lazy避免对整个文件内容进行任何操作,each_slice获取批处理大小的单词数组。
batch_size = 1000

STDIN.each(" ").lazy.each_slice(batch_size) do |batch|
  # batch is an Array of batch_size words
end

除了使用 cat 和 |,您也可以直接读取文件:

batch_size = 1000

File.open('huge_text_file.txt').each(" ").lazy.each_slice(batch_size) do |batch|
  # batch is an Array of batch_size words
end

使用此代码,不会拆分任何数字,无需逻辑,应该比逐个字符读取文件快得多,并且使用的内存要比将整个文件读入字符串少得多。

谢谢!它更快,但实际上与gets几乎相同,只是使用空格而不是\n作为分隔符。在这个问题中,分隔符可以是除数字以外的任何东西,并且偶尔的空格或换行符之间的字符串可能很大,因此我们可能会很快耗尽内存。有没有一种方法可以自动通过文本进行正则表达式匹配,而无需全部读取它? :) .each(" ").gets如何做到这一点?他们读取一个块,然后拆分并且在分隔符之后丢弃一部分吗? P.S. 管道用于灵活性,因此我可以链接命令或从curl获取输入。 - epsylon
然后你需要按固定长度读取数据块,并查看是否分割了一个数字。 - Eric Duminil
看起来是这样的...顺便提一下,使用each("\n").each_slice(batch_size)确实比使用f.each_line要快一点,但比使用.gets慢一半。我想这是因为创建了中间对象。 - epsylon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接