この記事はPerl Advent Calendar 2019の13日目の記事です。
12日目は@MacOlinさんの「Win32::OLEを使ったMicrosoft Wordの操作」でした。
ちなみに、全然関係ないですけど今日って13日の金曜日なんですね!!
結論(伝えたいこと)
Tie::Fileを使うと、ファイルの行に直接アクセスできる。
大容量ファイルへアクセスするときに便利。
さらに、読み込み時のキャッシュを多くすれば、ディスクI/Oを低減できてスピードアップする。
きっかけ
プログラミング課題で、「省メモリで大容量のCSVファイルを短時間で処理するためのバッチプログラムを書いてほしい」というものがありました。
単純にメモリに格納するのでは効率が悪いと考えて色々試していたところ、Tie::Fileモジュールを使うとファイルの中身をすべてメモリに展開せず、ファイルに行指定でアクセスできることを知りました。
課題には、「外部のミドルウェア(RDBなど)に頼らず、プログラムで処理すること」と制約があったので、「それならこんな感じかなぁ。。」と思い、書いてみました。
モジュールの公式ドキュメントはこちらです。
https://perldoc.perl.org/Tie/File.html
前提
課題の内容
ざっくり言うと、以下の3条件を満たすプログラムを書くことです。
- サイズ1GB、レコード数700万の2つのCSVファイルに共通して存在するカラムから、文字列が完全一致する行を抜き出して単一の別ファイルに両方のファイルの行を出力せよ
- プログラムの使用メモリは1GB以内に収めよ
- それなりに遅くならないようにせよ(速度を意識せよ、みたいなニュアンスです)
要するに、「SQLで言うところのinner joinをやる」という意味です。
実行環境
- [OS] Windows 10 Home
- [CPU] Intel64 Family 6 Model 158 Stepping 10 GenuineIntel ~2808 Mhz
- [Memory] 8GB
- [Perl] msys2上に構築
Tie::Fileをどこで使うか
inner joinの内部表を作るときに使います。
まず、読み込みファイルをオープンします
my $obj2 = tie(my @array2, 'Tie::File', $file2, memory => 20_000_000)
or die "File tie error : $!";
memory
を明示的に指定すると、キャッシュをデフォルトの2MBから変更することができます。今回は、20MBに調整してファイルI/Oの削減を試みました。
@array2
にはファイルの行情報が格納されているので、スカラで評価するとファイルの行数が取得できます。
適当な所で区切りながら、内部表を作ります。
# 内部表作成
my $inner_table = Match::InnerTable->new; # 内部表をハッシュ形式で作成するための自作モジュール
# データは配列形式で格納されているため、ファイルの1行目は0になります。よって、$track_index2の初期値は0です
# $row_size2は@array2をスカラで評価して-1します
for my $i2 ($track_index2..$row_size2) {
my $line2 = $obj2->FETCH($i2); # 指定行を取り出す
my ($id2, $email, $smtp, $datetime, $login_id2) = split /,/, $line2;
.
.
.
# データ挿入
$inner_table->insert(key => $login_id2, value => $i2);
}
指定した列をキー、キーが存在する行番号を値にしたハッシュを作成します。
これで、値に一行丸ごと挿入しなくて良くなるので省メモリになります。
実際のコードでは、比較したいカラムを予めソートしておいたり、一回で読み込むと1GBを超えてしまったので適当なところで区切りを入れたりしていますが、割愛してます。
あとは、もう片方のファイルを駆動表としてforループで処理し、キーにマッチした行をFETCH
してゆけば良いです。実行環境では510secくらいで処理できました。メモリ使用量は700MBくらいでした。
おわりに
なぜ全てメモリに展開せずに行を取り出すことができるのか、モジュールの実装を追って確かめてみたいと思います。
Perl Advent Calendar 2019, 明日の担当は, @mackee_wさんです.