std::stringstreamのwrite(buf, 1000000)が遅いです

解決


山本尚弘  2008-01-31 12:07:12  No: 67390

皆様はじめまして山本尚弘と申します。

VC6にてstringstreamを便利に使っていたんですが、
1MBなど、ちょっと大きめな値を書き込もうとすると遅いんです。
write(buf, 1000000);//1メガ
とするだけでなく、
write(buf, 100000);//100キロ
などと小分けにしてwriteしていっても、
だんだんと処理時間が長くなります。
(ちなみにwriteするのは文字列ではなくバイナリデータです)

質問その1.
もしかしてstringstreamは
クラス名からして、stringしかもそれ程大きくない文字列しか想定していないのでしょうか?
それとも、スピードアップさせる裏技があるのでしょうか?

質問その2.
みなさまは、バイナリデータをメモリ上のストリームで持つ時は
どんなクラスを使っていますか?
それとも使わずに、char* buf;でしょうか?

仕様環境は
WinXP,Pen4,1GByteです。


επιστημη  URL  2008-01-31 19:37:35  No: 67391

うーん...おそらく、ですけどstringstreamが腹ん中に抱えているstringbufが内包するstringが長くなるにしたがってメモリの再割り当てが起こります。そのせいじゃないかと思うです。

最終的な長さNがあらかじめわかっているならば、
string strbuf;
strbuf.reserve(N); // N文字の領域をあらかじめ確保
strbuf.append(buf, 100000); // ケツに追加
...

ってな使い方がいっちゃん早くてシンプルではないかなーと。


山本尚弘  2008-02-01 14:29:09  No: 67392

επιστημηさま
ご教授ありがとうございます

write(buf, 1000000);
とすると一気に1MB増やしての再割り当てをするのではなく、
内部的に少しずつ増やして何度何度も再割り当てしてるということでよろしいでしょうか?
であれば、この遅さ、納得です。
(実装系に依存もするし定かではないということですね)

ただ今回は困ったことに、
最後までデータのサイズがいくつになるかわからない状況なので、
上で教えていただいた
stringに予めバッファを確保しておく方法だと、
1.増分による更なる再割り当て時と
2.確保したバッファよりデータが小さかった時に
今まで何バイト書き込んだかを覚えておかないとダメということですよね。
(tellp()があればいいのですが。)

困りました。
char*を内包するRead,Writeするだけのクラスを
作った方が手っ取り早いでしょうか。


επιστημη  URL  2008-02-01 18:44:39  No: 67393

> write(buf, 1000000);
> とすると一気に1MB増やしての再割り当てをするのではなく、
> 内部的に少しずつ増やして何度何度も再割り当てしてるということでよろしいでしょうか?

ちゃいます。
writeされるたびに、追加分でバッファが溢れるようなら拡張します。
どのくらい拡張されるかは実装次第なのですが、大抵の実装では"現サイズの2倍"みたいです。

> stringに予めバッファを確保しておく方法だと、
> 1.増分による更なる再割り当て時と
> 2.確保したバッファよりデータが小さかった時に
> 今まで何バイト書き込んだかを覚えておかないとダメということですよね。

ちゃいます。
reserveでNバイト確保しておけば
"その量に達するまではメモリ再割り当てを行わない"
てことです。Nを超せば再割り当てが行われます。

> char*を内包するRead,Writeするだけのクラスを
> 作った方が手っ取り早いでしょうか。

わかんない。
最終サイズがわかんないのに、なぜこれだとOK?

巨大文字列を扱うなら、STLportが提供する rope なんか使えるかも。


ケン  2008-02-02 12:25:52  No: 67394

素朴な疑問ですけど、メモリ上で読み書きするのに、たかが1MBぐらいでほんとに実感できるほど遅いんでしょうか?
> WinXP,Pen4,1GByte
ということはDDRで空きメモリも十分だと思いますが、なんか他に原因があるような気もします。私の勘違いならすみませんが。


ななし  2008-02-02 20:31:27  No: 67395

メモリの確保/再確保は遅い。
計測してみるといいと思う。
物理アドレス直でリニアに確保/再確保してる(できる)わけじゃないし。


επιστημη  2008-02-02 20:38:12  No: 67396

どーしても気になるなら自前のallocatorを用意するですね ^^;


ケン  2008-02-03 00:18:46  No: 67397

>メモリの確保/再確保は遅い。
ええ、そこら辺は知ってるんですけど、でもベンチのように意味の無く同じ処理を100万回ぐらい繰り返しでもしなければ「体感」するのは不可能だと思うんですよね。

>計測してみるといいと思う。
も結局100万回繰り返して合計時間を計るような話ですよね。もちろん質問主のアプリが100万回繰り返すような処理なら話は別ですが。


山本尚弘  2008-02-03 14:35:39  No: 67398

レスをいただいた皆様、ありがとうございます。

>メモリ上で読み書きするのに、たかが1MBぐらいでほんとに実感できるほど遅いんでしょうか?

今、出先なのですがBorlandのコンパイラで実験したところ、
自宅のMSVC++6の時と結果が異なりました。

自宅だとwrite(buf, 1000000)が、PCがフリーズしたように遅かったのですが
出先だと目に見えた速度の低下はありませんでした。

皆さんの環境でも遅いのだろうと思い込んでいたんですが、
PCやコンパイラの環境問題のようですね。

ちょっと以下のコードを自宅でもう一度行って、結果をご報告します

  char* aaa = (char*)malloc(1000000);
  char bbb[1000000];
  memset(aaa, 0x41, 1000000);
  memcpy(bbb, aaa, 1000000);
  std::stringstream ccc("");
  ccc.write(bbb, 1000000);
  char ddd = ccc.str().c_str()[999999];
  free(aaa);

>επιστημηさん
「拡張されるのは"現サイズの2倍"」ということですが
1Mまで倍々で拡張されるということでしょうか?


επιστημη  URL  2008-02-03 19:28:43  No: 67399

>「拡張されるのは"現サイズの2倍"」ということですが
> 1Mまで倍々で拡張されるということでしょうか?

だから実装次第だってば。
やってみるっきゃないやんかー

#include <iostream>
#include <iomanip>
#include <string>

using namespace std;

int main() {
  string s;
  size_t capacity = 0;
  int reallocs = 0;
  for ( int i = 0; i < 1024*1024; ++i ) {
    s.reserve(i);
    if ( capacity != s.capacity() ) {
      ++reallocs;
      capacity = s.capacity();
      cout << setw(10) << i
           << setw(10) << capacity << endl;
    }
  }
  cout << reallocs << " reallocations\n";
}

VC++でやってみた。

         0        15
        16        31
        32        47
        48        70
        71       105
       106       157
       158       235
       236       352
       353       528
       529       792
       793      1188
      1189      1782
      1783      2673
      2674      4009
      4010      6013
      6014      9019
      9020     13528
     13529     20292
     20293     30438
     30439     45657
     45658     68485
     68486    102727
    102728    154090
    154091    231135
    231136    346702
    346703    520053
    520054    780079
    780080   1170118
28times reallocations

きっちり二倍ってことではなさそうですねー


ケン  2008-02-04 00:26:15  No: 67400

>PCやコンパイラの環境問題のようですね。
VC6はDebugビルドのみです?もしそうならReleaseでも試してみて下さい。

無料のVC2008expressとかでも試せればいいのですが、expressではstringstreamは使えないのかな?


επιστημη  URL  2008-02-04 03:00:53  No: 67401

> expressではstringstreamは使えないのかな?

標準C++ライブラリに属するので express でもおっけぇです。


山本尚弘  2008-02-04 12:31:41  No: 67402

山本です。

まだ自宅に帰れないでいるんですが、、

>επιστημηさま
わざわざコードまで示して頂いてありがとうございます。
なるほどぉ。VCだと上記のようにReAllocするんですね。
普段考えもしてませんでした。

元々、s.reserve(1000000);とした場合は、
倍々にReAllocして行くのかなという疑問だったのですが
よく考えてみればさすがに、そんな馬鹿げた実装はないですよね

>ケンさま
>VC6はDebugビルドのみです?もしそうならReleaseでも試してみて下さい。
ああっとなるほど、コレ試してませんでした。自宅に戻り次第実験いたします。

ただ、メイン開発環境のVC6debugで
stringstreamが現実的に使えないとなると不便だなぁ。。
再インストールしたら直ったりして。


yoh2  2008-02-05 07:51:05  No: 67403

以下のソースを、最適化オプション /Ox /G6 を付けたVC++6SP6でコンパイルして
プロファイルを取ってみました。
# リリースモード時の速度を気にされていたようですので、最適化ありにしてみました。

#include <sstream>

char g_bytes[1024 * 1024];

int main(int, char *[])
{
    std::stringstream ss;
    ss.write(g_bytes, sizeof(g_bytes));
    return 0;
}

結果(目立つところだけ; Pentium4 3.0GHzで30秒かかってます):
        関数          関数+チィルド           ヒット
        時間   %         時間      %      カウント  関数
----------------------------------------------------
    1408.518   4.7     1408.518   4.7    32770 operator delete(void *) (delop.obj)
   28824.382  95.3    30232.811  99.9    32768 std::basic_stringbuf<char,struct std::char_traits<char>,class std::allocator<char> >::overflow(int) (stringstream_write_test.obj)

見ての通り、stringbuf::overflow()とoperator delete()がやたらめったら
呼ばれています。

ソースを追い掛けてみたところ、stringbuf::overflow()は、バッファを一度に
32バイトしか増やさない様子。
つまり、1MB / 32B = 32768回だけバッファ再確保→コピー→旧バッファ開放なんて
やっているという寸法。そりゃ遅くもなるわけで。
うまくアロケータを工夫すれば、メモリ確保/解放の回数は減らせそうな気はしますが、
コピーの回数は変わりませんね。

ちなみに、通るソースは別になりますが、επιστημηさんのコードで調べている
string::reserve()も32バイト単位な模様。


山本尚弘  2008-02-05 13:53:03  No: 67404

山本です

>yoh2さま
とてもわかりやすいレポートありがとうございました。

>32バイトしか増やさない様子。
想像以上の実装でがっくりきました。

というかstringstreamに対して
私のような使い方が間違っている気がしてきました

おとなしく
char* buf = (char*)malloc(1000000);
として行こうと思います。。


επιστημη  2008-02-05 20:17:26  No: 67405

> おとなしく
> char* buf = (char*)malloc(1000000);
> として行こうと思います。。

「最後までデータのサイズがいくつになるかわからない状況」じゃなかったのん?


yoh2  2008-02-06 06:28:27  No: 67406

stringstreamにこだわるのでなければ、vector<char>を使うのも手かもしれません。
back_inserter() (back_insert_iteratorクラス) や、vector::push_back()を
使えば、メモリの再確保はSTLにまかせっきりにできますし。

#include <vector>
#include <iterator>
#include <algorithm>

char g_bytes[1024 * 1024];

int main()
{
    std::vector<char> v;
    std::copy(g_bytes, g_bytes + sizeof(g_bytes), std::back_inserter(v));
    return 0;
}

これはVC++6でも一瞬で終わりました。
内部で呼び出されている、vector<char>::push_back()は、stringbufと異なり
バッファ不足時、バッファサイズを倍々に増やしているようです。

もっとも、VC++6から他の処理系に移ったときにVC++6のstringstreab::write()のように
大幅な速度低下が起こらないかどうかは保証できませんけど。


山本尚弘  2008-02-06 09:51:14  No: 67407

山本です

>επιστημηさま
はい、全て自前で確保解放しようという意気込みです。
自分好みのBufferクラスを作ろうかな〜

>yoh2さま
Vectorってこういう使い方もできたんですね。
またまた勉強になりました。

その他レスを下さった方々、ありがとうございました。
全て疑問が解決しました。
すっきりです〜


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加