LoginSignup
2
1

More than 3 years have passed since last update.

Tie::Fileで大容量ファイルを処理する

Last updated at Posted at 2019-12-12

この記事は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の内部表を作るときに使います。

まず、読み込みファイルをオープンします

fopen.pl
  my $obj2 = tie(my @array2, 'Tie::File', $file2, memory => 20_000_000)
    or die "File tie error : $!";

memoryを明示的に指定すると、キャッシュをデフォルトの2MBから変更することができます。今回は、20MBに調整してファイルI/Oの削減を試みました。

@array2にはファイルの行情報が格納されているので、スカラで評価するとファイルの行数が取得できます。

適当な所で区切りながら、内部表を作ります。

create_inner_table.pl
    # 内部表作成
    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さんです.

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1