Rcpp でファイルの行数をカウントする(2)

前回に引き続きファイルの行数のカウントに取り組みます。

前回は getline() と istreambuf_iterator() を使った方法を紹介しましたが、残念ながら Unix コマンド wc には負けてしまいました。これをこのままにしておくのも口惜しいので、C言語で書かれた wc のソースコードからファイル行数をカウントする部分を参考にして、それを関数として実装しなおしました。

ソースを見ると wc コマンドではファイルを決められた文字数ずつメモリに読み込みながら行数をカウントしていました。そこで、この一度に読み込む文字数をユーザーが変えられるように Rcpp の関数にしてみました。

rcpp_wc.cpp

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
unsigned int rcpp_wc(std::string file, int buf_size=10000) {
  //file : ファイルへのパス
  //buf_size : 一度の読みこむ文字数
  
  //ファイルを開く
  FILE *fp;
  fp = fopen (file.c_str(), "r");
  
  uintmax_t lines=0;//行数のカウント用
  char buf[buf_size];//読み込んだ文字を保存する変数
  size_t bytes_read;//実際に読み込まれたバイト数(文字数)を格納する変数

  //行が長い場合には、行の数え方を変えるためのフラグ
  bool long_lines = false;
  
  //ファイルをから buf_size ずつ文字列を読み込んでゆく
  while ((bytes_read = fread(buf, 1, buf_size, fp)) > 0)
  {
    char *p = buf; //ポインタ p にバッファーの先頭文字のアドレスを与える
    char *end = p + bytes_read; //このステップで読み込まれた文字列の最後の文字へのアドレスを保持する
    uintmax_t plines = lines;   //前のステップで読み込んだ文字列中にあった改行の数を plines に保存しておく
    
    // 改行 '\n' の数を数える
    if (! long_lines)//1行が短い場合
    {//文字列を1文字ずつ走査して改行をカウント
      while (p != end)
        lines += *p++ == '\n';
    } else {//1行が長い場合
      // memchr()の呼び出しにかかる時間を考慮しても memchr() を使ったほうが速度が速い
      // memchr(p, '\n', n) は位置 p にある文字から n 文字先のまでの範囲から '\n' を探して
      // 最初に見つけた '\n' の位置を返す関数
      // reinterpret_cast<char*>() は memchr() の返値の型である void* を char* として解釈するように命令している
      while ((p = reinterpret_cast<char*>(memchr (p, '\n', end - p)))) // 最初に見つかる '\n' を探す
      {
        ++p;//見つけた '\n' の次の文字に移る
        ++lines;//見つけた '\n' の数(行数)をカウントアップ
      }
    }
    
    //バッファー中に含まれる行数の平均値が 15 以上になったら、
    //次のステップからは memchr() を使うようにフラグを立てる
    if (lines - plines <= bytes_read / 15)
      long_lines = true;
    else
      long_lines = false;
    //次のステップへ
  }
  return lines;
}

では、これを元の wc コマンドと比較してみましょう。一度に読み込む文字数を変えると行数のカウントのスピードはどう変わるでしょうか。

sourceCpp("rcpp_wc.cpp")
microbenchmark::microbenchmark(
  system("wc -l hoge.csv"),
  rcpp_wc("hoge.csv",10000),
  rcpp_wc("hoge.csv",1000),
  rcpp_wc("hoge.csv",100),
  rcpp_wc("hoge.csv",10),
  times=5)

実行結果

Unit: milliseconds
                       expr        min         lq       mean     median         uq        max neval
   system("wc -l hoge.csv")  1340.7916  1403.8807  1735.8336  1498.6164  2183.7768  2252.1026     5
 rcpp_wc("hoge.csv", 10000)   345.6389   382.7236   476.2079   388.7028   423.4235   840.5509     5
  rcpp_wc("hoge.csv", 1000)   601.1652   618.4181   675.9131   632.3813   655.2329   872.3680     5
   rcpp_wc("hoge.csv", 100)  1725.2825  1728.1933  1799.3132  1816.1720  1820.8418  1906.0762     5
    rcpp_wc("hoge.csv", 10) 11762.0427 11845.1454 12273.0468 12159.5957 12731.1063 12867.3437     5

おおっ!ついに wc に勝利しました。一度に読み込む文字数を増やすと行数カウントのスピードも伸びていることがわかります。かなり劇的な効果です。hoge.csv は600MB程度のサイズですが、この程度なら瞬殺できることがわかります。こんなに速くなるとは思いませんでした。テストマシンがしょぼいのでこれ以上大きなファイルでは試してないですが、かなり大きなファイルでもこれは期待できそうです。

いやー、これでまた業務が捗ってしまいますね(ドヤ)。

ということで、Rcpp の凄さの一端は感じていただけたかと思いますが、コードが Rcpp というより C/C++ そのまんまな感じになってしまったので、次回はもうちょっと Rcpp の機能を利用したコードを紹介したいと思います。

でわー