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 の機能を利用したコードを紹介したいと思います。
でわー