ファイルを「丸呑み」することが通常のテキストファイル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
テキストファイルを読み込んで1行ずつ処理することに関する質問が何度もあり、read
またはreadlines
のバリエーションを使用して、ファイル全体を1つのアクションでメモリにプルします。
read
のドキュメント のコメント:
ファイルを開き、オプションで指定されたオフセットを探し、長さバイトを返します(デフォルトはファイルの残りの部分です)。 [...]
readlines
のドキュメント のコメント:
名前で指定されたファイル全体を個別の行として読み取り、それらの行を配列で返します。 [...]
小さなファイルをプルすることは大したことではありませんが、着信データのバッファーが大きくなるにつれてメモリをシャッフルする必要があり、CPU時間を消費します。さらに、データが多くのスペースを消費する場合、OSはスクリプトを実行し続けるために関与し、ディスクへのスプールを開始する必要があります。これにより、プログラムが限界に達します。 HTTPd(web-Host)または高速応答が必要なものでは、アプリケーション全体が機能しなくなります。
スラッピングは通常、ファイルI/Oの速度の誤解、または一度に1行ずつ読み取るよりも、バッファーを読み取ってから分割する方が良いとの考えに基づいています。
以下は、「丸呑み」によって引き起こされる問題を示すためのテストコードです。
これを「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
サイズが大きくなる5つのファイルを作成します。 1Kファイルは簡単に処理でき、非常に一般的です。以前は1MBのファイルは大きいと考えられていましたが、現在では一般的です。私の環境では1 GBが一般的であり、10 GBを超えるファイルが定期的に検出されるため、1 GB以上で何が起こるかを知ることは非常に重要です。
これを「readlines.rb」として保存します。それは何もしませんが、ファイル全体を1行ずつ内部的に読み取り、それを配列に追加して返されます。これはすべて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
は、ファイルサイズが増加するたびに2倍の速度で実行され、foreach
を使用すると直線的に速度が低下することに注意してください。 1MBで、「スラッピング」I/Oに影響を及ぼし、行ごとの読み取りに影響を与えないものがあることがわかります。また、最近では1MBのファイルが非常に一般的になっているため、前もって考えないと、プログラムの存続期間中にファイルの処理が遅くなることは簡単にわかります。ここで数秒かかる場合もあれば、1回発生する場合も多くはありませんが、1分間に複数回発生する場合は、年末までにパフォーマンスに重大な影響を与えます。
何年も前に大きなデータファイルを処理しているときにこの問題に遭遇しました。私が使用していたPerlコードは、ファイルのロード中にメモリを再割り当てしたため、定期的に停止しました。データファイルを丸呑みしないようにコードを書き換え、代わりに1行ずつ読み取って処理すると、実行時間が5分以上から1分未満に大幅に短縮され、大きな教訓が得られました。
特に行の境界を越えて何かをしなければならない場合は特に、ファイルの「丸呑み」が役立つことがありますが、そうする必要がある場合は、ファイルを読み取る別の方法を考えるのに時間をかける価値があります。たとえば、最後の「n」行から構築された小さなバッファーを維持し、それをスキャンすることを検討してください。これにより、ファイル全体を読み取って保持しようとすることによるメモリ管理の問題が回避されます。これについては、Perl関連のブログ「 Perl Slurp-Eaze "」で説明されています。このブログでは、完全なファイル読み取りを使用して正当化する「時期」と「理由」について説明し、Rubyにも適用されます。
ファイルを「丸呑み」にしない他の優れた理由については、「 パターンのファイルテキストを検索し、指定された値で置き換える方法 」を参照してください。
これは少し古いですが、入力ファイルを丸呑みするとプログラムがパイプラインに実質的に役に立たなくなるということを誰も述べていないことに少し驚いています。パイプラインでは、入力ファイルは小さいものの遅い可能性があります。プログラムが丸呑みしている場合は、データが使用可能になったときにデータを処理しておらず、入力が完了するまでに時間がかかる可能性があるまで待機することになります。どのぐらいの間?大きな階層でgrep
またはfind
を実行している場合、それは何時間または何日か、多かれ少なかれ、何でもかまいません。無限ファイルのように、完了しないように設計することもできます。例えば、 journalctl -f
は、システムで発生したイベントを停止せずに出力し続けます。 tshark
は、ネットワークで発生しているものをすべて停止せずに出力します。 ping
は、pingを停止せずに続行します。 /dev/zero
は無限、/dev/urandom
は無限です。
プログラムがそれを読み終えるまでおそらくプログラムはとにかく何もできないので、私が許容できるものとして丸呑みを見ることができる唯一の時間は、おそらく構成ファイルにあるでしょう。
なぜファイルを「丸呑み」することが通常のテキストファイルI/Oに適さないのか
ティンマンはそれを正しく当てます。私も追加したいと思います:
多くの場合、ファイル全体をメモリに読み込むのは扱いにくいです(ファイルが大きすぎるか、文字列操作に指数O()スペースがあるため)
多くの場合、ファイルサイズは予測できません(上記の特殊なケース)
常にメモリ使用量を認識しておく必要があります。別のオプションが存在する場合(たとえば、行単位)、すべてのファイルを一度に(些細な状況でも)読み取ることはお勧めできません。経験上、VBSはこの意味で恐ろしいものであり、コマンドラインを介してファイルを操作する必要があります。
この概念はファイルだけでなく、メモリサイズが急速に増加し、一度に各反復(または行)を処理する必要がある他のプロセスにも適用されます。 ジェネレーター関数 メモリ内のすべてのデータを処理しないように、プロセスまたは行の読み取りを1つずつ処理することにより、ユーザーを支援します。
余談ですが、Python is in smart in reading files in and in open()
method is to be line by by line by byデフォルト。ジェネレーター関数の適切な使用例を説明する「 Pythonの改善: 'yield'とジェネレーターの説明 」を参照してください。