为什么“啜食”一个文件不是一个好习惯?
为什么“啜食”一个文件对于正常的文本文件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
我们一次又一次地看到一些关于读取文本文件以逐行处理文本文件的问题,这些文件使用read
或read
线的变体,这些read
或read
线在一个动作中将整个文件拉入内存。
read
文档说:
打开文件,可select查找给定的偏移量,然后返回长度字节(默认为文件的其余部分)。 […]
readlines
的文档说:
将名称指定的整个文件读取为单独的行,并将这些行返回到数组中。 […]
拉一个小文件没有什么大不了的,但是随着input数据缓冲区的增长,内存必须被洗牌,并且会占用CPU时间。 另外,如果数据占用空间太大,操作系统不得不让脚本运行,并开始将文件caching到磁盘,这将使程序瘫痪。 在HTTPd(networking主机)或其他需要快速响应的应用程序中,会使整个应用程序崩溃。
Slurping通常基于对文件I / O速度的误解,或者认为读取和分割缓冲区比一次读取一行更好。
这里有一些testing代码来演示由“sl”“造成的问题。
保存为“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
它创build了五个增加大小的文件。 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代码会在加载文件时重新分配内存时会周期性地停止。 重写代码不要sl the数据文件,而是逐行读取和处理,从超过五分钟的速度提高到less于一个,给我一个很大的教训。
“啜食”一个文件有时是有用的,尤其是如果你必须跨越界限来做一些事情的话,如果你必须这样做的话,花些时间考虑阅读一个文件的方法是值得的。 例如,考虑维护一个由最后的“n”行构build的小缓冲区并对其进行扫描。 这将避免尝试读取和保存整个文件引起的内存pipe理问题。 这在Perl相关的博客“ Perl Slurp-Eaze ”中进行了讨论,它涵盖了“whens”和“whys”,以certificate使用完整的文件读取是正确的,并且适用于Ruby。
出于其他理由不要“啜泣”您的文件,请阅读“ 如何search一个模式的文件文本,并用一个给定的值replace它 ”。
为什么“啜食”一个文件对于正常的文本文件I / O不是一个好的做法
铁皮人击中了它。 我还想补充一下:
-
在许多情况下,将整个文件读入内存是不容易处理的(因为文件太大,或者string操作具有指数O()空间)
-
通常情况下,您无法预测文件大小(以上的特例)
-
你应该总是试着认识到内存的使用情况,如果存在一个替代选项(例如逐行),一次读取所有文件(即使是在微不足道的情况下)也不是好的做法。 我从经验中得知,VBS在这个意义上是非常可怕的,一个人被迫通过命令行操纵文件。
这个概念不仅适用于文件,也适用于任何其他内存大小快速增长的进程,并且您必须一次处理每个迭代(或行)。 生成器函数通过逐个处理进程或逐行读取来帮助您,从而不能处理内存中的所有数据。
另外,Python在读取文件时非常聪明,其open()
方法被devise为默认逐行读取。 请参阅“ 改进您的Python:”yield(生成器)和Generators Explained(生成器说明 ),它解释了生成器函数的良好用例示例。