C++でファイルを読み込むとき、std::ifstream
を使うのが一般的だと思います。本記事では、ifstream
でファイルを後ろ向きに読み込む場合には、バッファリングに気を付けて実装しないと読み込みがとても遅くなってしまうことについて解説します。
以下、固定長バッファを使いつつ文字を一文字ずつ後ろから読み込みたい場合を想定します。
TL;DR
ifstream
は内部でバッファリングしてくれてる。- 後ろ向きに読み込む場合はバッファリングがうまく機能せず激遅になる。
FILE
など使ってちゃんとバッファリングしながら読み込もう。
目次
方法
シンプルに前から読み込み
一文字だけ読み込みたい場合は get
関数が便利です。ifstream
を使ってファイルを前から一文字ずつ読み込む場合は、以下のようにループを回すだけです。
// std::ifstremによるシンプルな順方向読み込み void read_forward(const char* filename) { std::ifstream ifs(filename, std::ios::binary); for (char c; ifs.get(c);) { hogehoge(c); // 何かしらの処理 } ifs.close(); }
このとき ifstream
は内部で streambuf
がバッファリングしてくれてるので、我々は意識しなくても自動的に重たいファイル I/O を抑制してくれています。
例えば手元の AppleClang 12.0.0 ではバッファサイズが 1024 バイトでしたので、実際にファイルを参照するのは 1024 回に1 回で済みます。
シンプルに後ろ向きに読み込み
では同じように get
関数を使って、ファイルを逆から一文字ずつ読み込もうとするとき、以下のようなコードが書きたくなるかもしれません。というかググったら某QAサイトでいくつかこういう実装が見られました。
// std::ifstremによるシンプルな逆方向読み込み void read_backward(const char* filename) { std::ifstream ifs(filename, std::ios::binary); ifs.seekg(-1, std::ios::end); // 読み込みポインタを最後の文字に設定 for (char c; ifs.get(c);) { // 読み込みポインタ += 1 hogehoge(c); // 何かしらの処理 ifs.seekg(-2, std::ios::cur); // 読み込みポインタ -= 2 } ifs.close(); }
このコードではファイルを後ろ向きに読み込めるようにポインタを更新しつつ、素直に get
で一文字ずつ読み込んでます。
では実際に 1 MiB のファイルを読み込んだときの実行時間を計測してみます。すると以下のような結果となりました。(実験環境とコードへのリンクは記事の最後にあります)
関数 | 実行時間 |
---|---|
read_forward |
10 ms |
read_backward |
3426 ms |
後ろ向きの読み込みがベラボーに遅いです。
これは streambuf
が get
で参照した位置を先頭としてデータブロックをバッファに読み込むためです。一文字ずつ戻る read_backward
では結局 1 MiB 回の I/O が必要となって激重となってしまったようです。
バッファをちゃんと使って後ろ向きに読み込み
ということで後ろ向きにファイルを読む込むときには、単純に ifstream
に頼らずちゃんとバッファリングするように実装してあげる必要があります。
とりあえず ifstream
を使わず以下のように FILE
を使って書くのが最も簡単かと思います。
// FILEによるバッファを使った逆方向読み込み void read_backward_buffering(const char* filename) { // バッファ constexpr std::streamsize bufsize = 1024; char buffer[bufsize]; FILE* fp = fopen(filename, "rb"); fseek(fp, 0L, SEEK_END); // 読み込みポインタをファイル末尾に設定 const int64_t filesize = ftell(fp); // ファイルサイズ for (auto end = filesize; end > 0;) { const auto beg = std::max<int64_t>(0, end - bufsize); const auto readsize = end - beg; fseek(fp, -readsize, SEEK_CUR); // 読み込みポインタを設定 fread(buffer, 1, readsize, fp); // バッファに読み込み fseek(fp, -readsize, SEEK_CUR); // 読み込んだ分をリセット for (auto j = readsize - 1; j >= 0; --j) { hogehoge(buffer[j]); // 何かしらの処理 } end = beg; } fclose(fp); }
実行時間は以下のようになり、ちゃんと高速にファイルが後ろ向きに読み込めているのがわかります。read_forward
よりも速いのは単純に ifstream
より処理が簡潔だったからでしょう。
関数 | 実行時間 |
---|---|
read_forward |
10 ms |
read_backward |
3426 ms |
read_backward_buffering |
3 ms |
まとめ
というわけで C++ で少しトリッキーなファイル入出力をする場合には、fstream
のバッファリングに気をつけようという話でした。
今回は FILE
を使って解決しましたが、もっと C++ らしく書きたい場合は、std::basic_streambuf
から派生して独自の stream
クラスを実装すればいいのではないかと思います。
実験環境
- Machine: MacBook Pro (13-inch, 2019)
- OS: macOS Catalina (version 10.15)
- Processor: 2.4 GHz Quad-Core Intel Core i5
- Memory: 16 GB 2133 MHz LPDDR3
- Compiler: AppleClang (version 12.0.0)
- Flags:
-O3