C++のstreamの転送速度を調べる
はじめに
C++のstreamはとても良くできていて、これを用いたライブラリを作りたいのだけど、
本当に(主にパフォーマンス的な理由で)大丈夫なのとかそういう話。
初めにお断りしておきますが、以下の内容はすべてlinux+gcc4.3での話です。
streamは遅い
ふつうにistreamからget()して、ostreamにputしてるとめちゃくちゃ遅い。
C言語のgetchar, putcharより10進数で1.5桁ぐらい遅いよ。
istream::readとかででかいブロック読めば大丈夫なのだけど、
細かい単位で読みたいことの方が多いよね。
そういうわけで、そういう場合にも速く転送することが可能なのかどうか調べてみる。
テストプログラム
istreamの内容をostreamに転送するプログラムを6通り書いた。
その1:普通のプログラム
オーソドックスなプログラム。
void copy1(ostream &os, istream &is) { for (char c; is.get(c); os<<c); }
その2:ブロックごとに読み書き
今回の趣旨には会わないが速度の比較のため。
(readに失敗したときの処理があってるのか不明)
void copy2(ostream &os, istream &is) { for (;;){ char buf[1024]; if (!is.read(buf, 1024)) break; os.write(buf, 1024); } copy1(os, is); }
その3:istream_iteratorを使う
is>>c; os<
void copy3(ostream &os, istream &is) { istream_iterator<char> p(is), end; ostream_iterator<char> q(os); copy(p, end, q); }
その4:istreambuf_iteratorを使う
Effective STLおすすめの方法。
空白は大丈夫です。
void copy4(ostream &os, istream &is) { istreambuf_iterator<char> p(is), end; ostreambuf_iterator<char> q(os); copy(p, end, q); }
その5:ostreamにstreambufを突っ込む
あんまり柔軟性は無いけど、
この書き方がどのぐらいの速度なのか調べたかった。
void copy5(ostream &os, istream &is)
{
os<<is.rdbuf();
}
その6:istreamからstreambufに突っ込む
5と同じぐらいの速度になるという予測。
void copy6(ostream &os, istream &is)
{
is>>os.rdbuf();
}
六つ書いたけど、期待しているのはその4だけです。
これがどういうケースで速くなるのかを調べたい。
テスト
これらのルーチンを、ifstream&ofstreamと、cin&cout、
さらにそれぞれ組み合わせと、出力が/dev/nullになってるときの
計8通りでテスト。単位は秒。
ファイルサイズは1GB、
メモリ8GBのマシンにおいて実行。
ファイルの内容はキャッシュにすべて乗った状況ですので、
純粋にCPUの計算量のみが考慮されるものと考えてください。
file -> file(普通のファイル)
copy1 | 40.310 |
copy2 | 3.340 |
copy3 | 46.900 |
copy4 | 1.550 |
copy5 | 1.870 |
copy6 | 1.620 |
file -> file(/dev/null)
copy1 | 38.610 |
copy2 | 0.880 |
copy3 | 45.390 |
copy4 | 0.400 |
copy5 | 0.390 |
copy6 | 0.410 |
file -> stdout(普通のファイルにリダイレクト)
copy1 | 47.780 |
copy2 | 3.560 |
copy3 | 53.990 |
copy4 | 3.330 |
copy5 | 4.120 |
copy6 | 4.060 |
file -> stdout(/dev/nullにリダイレクト)
copy1 | 56.530 |
copy2 | 0.700 |
copy3 | 66.580 |
copy4 | 0.510 |
copy5 | 0.500 |
copy6 | 0.510 |
stdin -> file
copy1 | 69.860 |
copy2 | 3.390 |
copy3 | 98.580 |
copy4 | 34.540 |
copy5 | 35.980 |
copy6 | 34.880 |
stdin -> /dev/null
copy1 | 68.000 |
copy2 | 0.890 |
copy3 | 100.190 |
copy4 | 34.180 |
copy5 | 34.040 |
copy6 | 34.410 |
stdin -> stdout(file)
copy1 | とても長い(>300) |
copy2 | 6.080 |
copy3 | とても長い |
copy4 | 46.160 |
copy5 | 70.310 |
copy6 | 70.530 |
stdin -> stdout(/dev/null)
copy1 | とても長い |
copy2 | 0.870 |
copy3 | とても長い |
copy4 | 44.830 |
copy5 | 44.210 |
copy6 | 44.350 |
とても長いと書いてあるところは、長すぎたので切りました。
5分で切りましたが、多分10分たってもおわらないのじゃないかな。
考察
出力が/dev/nullか普通のファイルかは当然/dev/nullの方が速いものの、
systemの時間が変わるだけです。
計測する必要なかったな。
ブロック転送(copy2)はすべてにおいて速い。
これも当然ながら。
copy1はすべてにおいて遅い。
当然ながらほとんどuser時間。
copy4,5,6は同じぐらいの速度になった。
streambuf_iteratorだけ遅いという結果にならなくてよかった。
標準入力->標準出力で4と5,6で結果がだいぶ違う原因は不明。
以下はstreambuf_iteratorについて。
入力がifstreamの時はおしなべて速い。
入力がcinの時は速くない。
でも、cin.get()とかで読み取るよりは速い。
出力はofstreamでcoutでも大差はなし。
どちらかというとofstreamを使う方が速いようだ。
cin, coutが実際どういう型になっているのかは知らないが、
まあ実装がfstreamと違うのだろう。
速度の違いの理由を調べるため、straceをかけてみた。
基本的にどれもある程度まとまってread/writeされていた。
(サイズは8KB,4KB,1KB様々だが)
つまり、速度の違いはそれ以外のオーバーヘッドである。
しかし、許容しがたいほど遅かったもの(cin->coutのcopy1,3)は
出力が一文字ずつwriteされていた。
これは如何に。
ファイルからcoutへ転送する場合も再度調べたが、
たしかにバッファリングされている。
同じcoutへの出力なのになぜ?
調べるために、次の2つのコードを書いた。
for (char c; cin.get(c); ) cout<<c;
ifstream ifs("tmp"); for (char c; ifs.get(c); ) cout<<c;
すると、やはり、なんと、前者はバッファリングされず、後者はされるのだ。
私はこの結果を見たとき、
もうC++とはとっととおさらばしてユートピアへ行こう!
と思わずにはいられなかった。
今回この原因を探るのはめんどいのでしないが、
まあそのうち調べたいと思う。というか、調べないといかんのだろうなあ。
read/writeとの比較
FD間で直接転送した場合と比べてどうなのかを調べてみた。
void fdcopy(int to, int from) { char buf[1024*8]; for (int n; (n=read(from, buf, sizeof(buf)))>0; ){ char *p=buf, *q=p+n; while(p<q){ ssize_t ret=write(to, p, q-p); assert(ret>0); p+=ret; } } }
結果
file-> file | 1.280 |
file -> /dev/null | 0.390 |
stdin -> stdout | 2.430 |
stdin -> stdout(/dev/null) | 0.380 |
streambuf_iteratorのオーバーヘッドは1割程度と言うことになる。
(バッファのサイズが違ったりするので、単純には言えないけども)