为什么对普通文本文件I/O来说,文件"slurping"不是一个好的做法,什么时候有用?

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

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

推荐答案

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

The documentation for read说:

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

The documentation for readlines说:

将按名称指定的整个文件作为单独的行读取,并以数组形式返回这些行.[...]

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

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".它什么都不做,只是在内部逐行读取整个文件,并将其附加到一个数组中,然后返回该array.由于它都是用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代码在加载文件时会定期停止,因为它会重新分配内存.重新编写代码,使数据文件不发出声音,而是逐行读取和处理,极大地提高了运行速度,从五分钟多提高到不到一分钟,给了我很大的教训.

"slurping"一个文件有时很有用,尤其是当你必须跨越行边界做一些事情的时候,然而,如果你必须这样做的话,花一些时间考虑其他读取文件的方法是值得的.例如,考虑从最后的"N"行建立一个小缓冲区并扫描它.这将避免由于试图读取和保存整个文件而导致的内存管理问题.这在一个与Perl相关的博客"Perl Slurp-Eaze"中进行了讨论,该博客涵盖了"何时"和"为什么"来证明使用完整文件读取的合理性,并且很好地适用于Ruby.

如果你还有其他不想"gulp "你的文件的好理由,请阅读"How to search file text for a pattern and replace it with a given value".

Ruby相关问答推荐

CarrierWave Multi-uploader重命名现有文件

ruby 组合哈希相关键

这个#divmod 方法输出这个结果是做什么的?

为什么我的二维 Ruby 数组的多个值会发生变化,尽管只更改了其中一个?

Ruby 中无法解释的撬动行为

Ruby:如何将两个返回值连接到一行中的两个字符串

需要在Ruby中将数组拆分为指定大小的子数组

由模块中定义的另一个覆盖方法

在 RSpec-2.11 中使用隐含的 `subject` 和 `expect`

ruby 是否支持 case 语句中的范围?

为什么我们要在 Ruby 的类中放置一个模块?

从Electron邮件中删除签名和回复

如何仅从 Gemfile 中查看依赖关系树?

就地修改 ruby​​ 哈希(rails strong params)

使用 RSpec 判断某物是否是另一个对象的实例

运行 Ruby 命令时,PATH 中不安全的世界可写目录 /Users/username,模式 040777

从Ruby中的子类方法调用父类中的方法

OpenSSL vs GPG 用于加密异地备份?

在 Ruby 中获取用户主目录的跨平台方法?

为什么我们在 Ruby 中有 0.0 和 -0.0?