1 回のアクションでファイル全体をメモリに取り込むread、 またはのバリエーションを使用して、テキスト ファイルを読み取って行単位で処理する方法についての質問を何度も目にします。readlines
のドキュメントにreadは次のように書かれています:
ファイルを開き、オプションで指定されたオフセットをシークし、長さバイトを返します (デフォルトではファイルの残りの部分)。[...]
のドキュメントにreadlinesは次のように書かれています:
名前で指定されたファイル全体を個々の行として読み取り、それらの行を配列で返します。[...]
小さなファイルを取り込むことは大したことではありませんが、受信データのバッファが大きくなるにつれてメモリをシャッフルしなければならない時点が来て、CPU 時間が消費されます。さらに、データが大量のスペースを消費する場合、OS はスクリプトの実行を維持するためだけに関与する必要があり、ディスクへのスプールを開始します。これにより、プログラムが機能しなくなります。HTTPd (Web ホスト) または高速な応答が必要なものでは、アプリケーション全体が機能しなくなります。
丸呑みは通常、ファイル 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 のファイルが大きいと見なされていましたが、現在では一般的です。私の環境では 1GB が一般的で、10GB を超えるファイルが定期的に発生するため、1GB を超えるとどうなるかを知ることは非常に重要です。
これを「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 分間に複数回発生すると、1 年の終わりまでに深刻なパフォーマンスへの影響になります。
何年も前に、大きなデータ ファイルを処理しているときに、この問題に遭遇しました。私が使用していた Perl コードは、ファイルのロード中にメモリを再割り当てするため、定期的に停止していました。データ ファイルを丸呑みするのではなく、1 行ずつ読み取って処理するようにコードを書き直すと、実行時間が 5 分以上かかっていたのが 1 分未満になり、大きな教訓になりました。
ファイルの「丸呑み」は、特に行の境界を越えて何かをしなければならない場合に便利な場合があります。たとえば、最後の「n」行から構築された小さなバッファを維持し、それをスキャンすることを検討してください。これにより、ファイル全体を読み取って保持しようとすることによって発生するメモリ管理の問題を回避できます。これは、Perl 関連のブログ「Perl Slurp-Eaze」で説明されており、完全なファイル読み取りを使用することを正当化する「いつ」と「なぜ」をカバーしており、Ruby にも当てはまります。
ファイルを「丸呑み」してはいけないその他の優れた理由については、「ファイル テキストのパターンを検索し、それを特定の値に置き換える方法」を参照してください。