为什么“啜饮”文件不是一种好的做法?

时间:2014-08-07 18:09:26

标签: ruby io slurp

为什么"啜饮"一个文件不是普通文本文件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

3 个答案:

答案 0 :(得分:74)

我们一次又一次地看到有关阅读文本文件以逐行处理的问题,这些问题使用readreadlines的变体,将整个文件整合到一个内存中行动。

The documentation for read说:

  

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

The documentation for readlines说:

  

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

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

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

这里有一些测试代码来演示“啜饮”引起的问题。

将其另存为“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时,我们可以看到影响“啜饮”I / O的东西不会影响逐行读取。而且,因为现在1MB文件很常见,所以如果我们不提前考虑,很容易看到它们会在程序的整个生命周期内减慢文件的处理速度。这里或者几秒钟发生一次,但是如果它们每分钟发生一次,那么它会在一年之后产生严重的性能影响。

多年前我在处理大型数据文件时遇到了这个问题。我正在使用的Perl代码会在加载文件时重新分配内存时会定期停止。重写代码不会淹没数据文件,而是逐行读取和处理它,从五分钟以上的速度提高到小于一的速度,并教会了我一个重要的教训。

“啜饮”文件有时很有用,特别是如果你必须跨行界限做一些事情,但是,如果你不得不这样做,那么花一些时间考虑阅读文件的替代方法是值得的。例如,考虑维护从最后“n”行构建的小缓冲区并扫描它。这样可以避免因尝试读取和保存整个文件而导致的内存管理问题。这在Perl相关博客“Perl Slurp-Eaze”中进行了讨论,该博客涵盖了“whens”和“为什么”来证明使用完整的文件读取,并且适用于Ruby。

出于其他不要“捏造”文件的理由,请阅读“How to search file text for a pattern and replace it with a given value”。

答案 1 :(得分:3)

  

为什么“啜饮”文件不是普通文本文件I / O的好习惯

锡人正确地击中了它。我还想补充一下:

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

  • 很多时候,您无法预料到文件大小(上述特殊情况)

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

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

作为旁白/额外,Python在reading files in非常聪明,其open()方法设计为默认情况下逐行读取。请参阅“Improve Your Python: 'yield' and Generators Explained”,它解释了生成器函数的一个很好的用例示例。

答案 2 :(得分:1)

这有点陈旧,但令我感到有些惊讶的是,没有人提到将输入文件制成浆糊实际上会使程序对管道毫无用处。在管道中,输入文件可能很小但很慢。如果您的程序运行缓慢,则意味着它无法使用数据,因为它变得可用,而是让您等待输入完成所需的时间。多久?如果我在一个较大的层次结构中执行grepfind,则可能会是几小时或几天之类的任何事情。它也可以设计成不完整,就像一个无限的文件。例如,journalctl -f将继续输出系统中发生的任何事件而不会停止; tshark将输出网络中正在发生的一切,而不会停止; ping将继续执行ping操作而不会停止。 /dev/zero是无限的,/dev/urandom是无限的。

我只能看到配置文件是可以接受的,因为该程序在完成读取之前可能无法执行任何操作。