38

为什么“啜饮”文件对于普通的文本文件 I/O 来说不是一个好习惯,什么时候有用?

例如,为什么我不应该使用这些?

File.read('/path/to/text.txt').lines.each do |line|
  # do something with a line
end

或者

File.readlines('/path/to/text.txt').each do |line|
  # do something with a line
end
4

3 回答 3

83

我们一次又一次地看到有关读取文本文件以逐行处理它的问题,这些问题使用read或的变体readlines,它们在一个动作中将整个文件拉入内存。

的文档read说:

打开文件,可选择查找给定的偏移量,然后返回长度字节(默认为文件的其余部分)。[...]

的文档readlines说:

将 name 指定的整个文件作为单独的行读取,并在数组中返回这些行。[...]

拉入一个小文件没什么大不了的,但是随着传入数据的缓冲区的增长,内存必须被洗牌,这会占用 CPU 时间。此外,如果数据占用太多空间,操作系统必须介入以保持脚本运行并开始假脱机到磁盘,这将使程序瘫痪。在 HTTPd(网络主机)或需要快速响应的东西上,它会削弱整个应用程序。

Slurping 通常是基于对文件 I/O 速度的误解,或者认为读取然后拆分缓冲区比一次读取一行要好。

这是一些测试代码来演示由“slurping”引起的问题。

将此保存为“test.sh”:

echo Building test files...

yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000       > kb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000    > mb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt
cat gb1.txt gb1.txt > gb2.txt
cat gb1.txt gb2.txt > gb3.txt

echo Testing...

ruby -v

echo
for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt
do
  echo
  echo "Running: time ruby readlines.rb $i"
  time ruby readlines.rb $i
  echo '---------------------------------------'
  echo "Running: time ruby foreach.rb $i"
  time ruby foreach.rb $i
  echo
done

rm [km]b.txt gb[123].txt 

它会创建五个大小不断增加的文件。1K 文件很容易处理,而且很常见。过去 1MB 的文件被认为很大,但现在它们很常见。1GB 在我的环境中很常见,并且会定期遇到超过 10GB 的文件,因此了解 1GB 及以上会发生什么非常重要。

将此保存为“readlines.rb”。它什么也不做,只是在内部逐行读取整个文件,并将其附加到一个数组中,然后返回,并且看起来很快,因为它都是用 C 编写的:

lines = File.readlines(ARGV.shift).size
puts "#{ lines } lines read"

将此另存为“foreach.rb”:

lines = 0
File.foreach(ARGV.shift) { |l| lines += 1 }
puts "#{ lines } lines read"

在我的笔记本电脑上运行sh ./test.sh我得到:

Building test files...
Testing...
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]

读取 1K 文件:

Running: time ruby readlines.rb kb.txt
28 lines read

real    0m0.998s
user    0m0.386s
sys 0m0.594s
---------------------------------------
Running: time ruby foreach.rb kb.txt
28 lines read

real    0m1.019s
user    0m0.395s
sys 0m0.616s

读取 1MB 文件:

Running: time ruby readlines.rb mb.txt
27028 lines read

real    0m1.021s
user    0m0.398s
sys 0m0.611s
---------------------------------------
Running: time ruby foreach.rb mb.txt
27028 lines read

real    0m0.990s
user    0m0.391s
sys 0m0.591s

读取 1GB 文件:

Running: time ruby readlines.rb gb1.txt
27027028 lines read

real    0m19.407s
user    0m17.134s
sys 0m2.262s
---------------------------------------
Running: time ruby foreach.rb gb1.txt
27027028 lines read

real    0m10.378s
user    0m9.472s
sys 0m0.898s

读取 2GB 文件:

Running: time ruby readlines.rb gb2.txt
54054055 lines read

real    0m58.904s
user    0m54.718s
sys 0m4.029s
---------------------------------------
Running: time ruby foreach.rb gb2.txt
54054055 lines read

real    0m19.992s
user    0m18.765s
sys 0m1.194s

读取 3GB 文件:

Running: time ruby readlines.rb gb3.txt
81081082 lines read

real    2m7.260s
user    1m57.410s
sys 0m7.007s
---------------------------------------
Running: time ruby foreach.rb gb3.txt
81081082 lines read

real    0m33.116s
user    0m30.790s
sys 0m2.134s

请注意readlines,每次文件大小增加时,运行速度会减慢两倍,并且使用foreach速度会线性减慢。在 1MB 时,我们可以看到影响“slurping”I/O 的东西不会影响逐行读取。而且,由于 1MB 文件现在非常普遍,如果我们不提前考虑,很容易看出它们会在程序的整个生命周期内减慢文件的处理速度。当它们发生一次时,这里几秒钟或不多,但如果它们每分钟发生多次,到年底就会对性能产生严重影响。

几年前我在处理大型数据文件时遇到了这个问题。我使用的 Perl 代码在加载文件时会定期停止,因为它会重新分配内存。重写代码以不破坏数据文件,而是逐行读取和处理它,将运行速度从超过五分钟提高到不到一分钟,并给我上了一课。

“啜饮”文件有时很有用,尤其是当您必须跨行边界做某事时,但是,如果必须这样做,花一些时间考虑读取文件的替代方法是值得的。例如,考虑维护一个从最后“n”行构建的小缓冲区并对其进行扫描。这将避免由于尝试读取和保存整个文件而导致的内存管理问题。这在 Perl 相关的博客“ Perl Slurp-Eaze ”中进行了讨论,该博客涵盖了“何时”和“为什么”来证明使用完整文件读取的合理性,并且很好地适用于 Ruby。

出于其他不“啜饮”文件的绝佳理由,请阅读“如何在文件文本中搜索模式并将其替换为给定值”。

于 2014-08-07T18:10:43.370 回答
5

这有点老了,但我有点惊讶的是,没有人提到 slurping 输入文件会使程序实际上对管道毫无用处。在管道中,输入文件可能很小但很慢。如果您的程序正在slurping,则意味着它无法在数据可用时处理数据,而是让您等待输入完成所需的时间。多久?它可以是任何东西,比如几小时或几天,或多或少,如果我正在做一个grepfind在一个大的层次结构中。它也可以被设计成不完整的,就像一个无限的文件。例如,journalctl -f将继续输出系统中发生的任何事件而不会停止;tshark将在不停止的情况下输出它在网络中看到的任何情况;ping将继续 ping 不停止。/dev/zero是无限的,/dev/urandom是无限的。

唯一一次我认为 slurping 是可以接受的可能是在配置文件中,因为程序在完成读取之前可能无法做任何事情。

于 2018-09-04T21:09:53.280 回答
3

为什么“slurping”文件对于普通文本文件 I/O 来说不是一个好习惯

铁皮人打对了。我还想补充:

  • 在许多情况下,将整个文件读入内存是难以处理的(因为文件太大,或者字符串操作具有指数 O() 空间)

  • 通常,您无法预测文件大小(上述特殊情况)

  • 您应该始终尝试了解内存使用情况,如果存在替代选项(例如,逐行),则一次读取所有文件(即使在微不足道的情况下)不是好的做法。我从经验中知道 VBS 在这个意义上是可怕的,并且被迫通过命令行操作文件。

这个概念不仅适用于文件,还适用于内存大小快速增长并且您必须一次处理每个迭代(或行)的任何其他进程。生成器函数通过一一处理进程或行读取来帮助您,以免处理内存中的所有数据。

顺便说一句,Python 在读取文件方面非常聪明,它的open()方法被设计为默认逐行读取。请参阅“改进您的 Python:'yield' 和生成器解释”,它解释了生成器函数的一个很好的用例示例。

于 2014-08-07T18:25:11.227 回答