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割程度と言うことになる。
(バッファのサイズが違ったりするので、単純には言えないけども)

まとめ

まとめて読むことが可能なら、istream::read(), ostream::write()を使おう。


cinからのistreambuf_iterator経由での入力はあまり速くない。
でも、get()などを呼ぶことに比べたら速い。
ifstreamからのistreambuf_iteratorはとても速い。


ostreambuf_iteratorは大体とても速い。


結論としては、streambuf_iteratorは十分速い、十分使える。
つまり、stream使っといて大丈夫。