C ++標準では、iostreamのパフォーマンスの低下が義務付けられていますか?それとも、貧弱な実装を扱っているだけですか?


197

C ++標準ライブラリのiostreamのパフォーマンスが遅いことに言及するたびに、信じられないような波に遭遇します。それでも、iostreamライブラリコード(コンパイラの完全な最適化)に費やされた大量の時間を示すプロファイラー結果があり、iostreamからOS固有のI / O APIおよびカスタムバッファー管理に切り替えると、桁違いに改善されます。

C ++標準ライブラリはどのような追加の作業を行っていますか、それは標準で必要とされていますか、それは実際に役立ちますか?あるいは、一部のコンパイラーは、手動バッファー管理と競合するiostreamの実装を提供していますか?

ベンチマーク

問題を解決するために、iostreamの内部バッファリングを実行する短いプログラムをいくつか作成しました。

ostringstreamstringbufバージョンは、実行速度が非常に遅いため、実行される反復が少ないことに注意してください。

ideoneに、ostringstreamよりも約3倍遅いstd:copy+ back_inserter+ std::vector、および約15倍より遅いmemcpy生バッファへ。これは、実際のアプリケーションをカスタムバッファリングに切り替えたときのプロファイリング前後のプロファイルと一致しています。

これらはすべてメモリ内バッファーであるため、iostreamの速度が遅いディスクI / O、過度のフラッシュ、stdioとの同期、またはC ++標準ライブラリーの観察された速度の遅さを弁解するために使用するその他のものに起因するものではありません。 iostream。

他のシステムのベンチマークや、一般的な実装(gccのlibc ++、Visual C ++、Intel C ++など)が行うこと、および標準によってどの程度のオーバーヘッドが義務付けられているかについての解説を見るとよいでしょう。

このテストの根拠

多くの人々は、フォーマットされた出力にはiostreamがより一般的に使用されていると正しく指摘しています。ただし、これらは、バイナリファイルアクセス用のC ++標準によって提供される唯一の最新のAPIでもあります。しかし、内部バッファリングでパフォーマンステストを実行する本当の理由は、一般的なフォーマット済みI / Oに当てはまります:iostreamがディスクコントローラーにrawデータを供給できない場合、フォーマットを担当しているときに、どのようにして対応できるでしょうか?

ベンチマークのタイミング

これらはすべて、外側の(k)ループの反復ごとです。

ideone(gcc-4.3.4、不明なOSおよびハードウェア):

  • ostringstream:53ミリ秒
  • stringbuf:27ミリ秒
  • vector<char>およびback_inserter:17.6ミリ秒
  • vector<char> 通常のイテレータの場合:10.6 ms
  • vector<char> イテレータと境界チェック:11.4 ms
  • char[]:3.7ミリ秒

私のラップトップ(Visual C ++ 2010 x86 、、、cl /Ox /EHscWindows 7 Ultimate 64ビット、Intel Core i7、8 GB RAM):

  • ostringstream:73.4ミリ秒、71.6ミリ秒
  • stringbuf:21.7 ms、21.3 ms
  • vector<char>およびback_inserter:34.6 ms、34.4 ms
  • vector<char> 通常のイテレータの場合:1.10 ms、1.04 ms
  • vector<char> イテレータと境界チェック:1.11 ms、0.87 ms、1.12 ms、0.89 ms、1.02 ms、1.14 ms
  • char[]:1.48 ms、1.57 ms

プロファイルに基づく最適化を使用してVisual C ++ 2010のx86、 、cl /Ox /EHsc /GL /clink /ltcg:pgi実行、link /ltcg:pgo、対策:

  • ostringstream:61.2 ms、60.5 ms
  • vector<char> 通常のイテレータの場合:1.04 ms、1.03 ms

同じラップトップ、同じOS、cygwin gcc 4.3.4を使用g++ -O3

  • ostringstream:62.7 ms、60.5 ms
  • stringbuf:44.4 ms、44.5 ms
  • vector<char>およびback_inserter:13.5 ms、13.6 ms
  • vector<char> 通常のイテレータの場合:4.1 ms、3.9 ms
  • vector<char> イテレータと境界チェック:4.0 ms、4.0 ms
  • char[]:3.57 ms、3.75 ms

同じラップトップ、Visual C ++ 2008 SP1 cl /Ox /EHsc

  • ostringstream:88.7 ms、87.6 ms
  • stringbuf:23.3 ms、23.4 ms
  • vector<char>およびback_inserter:26.1 ms、24.5 ms
  • vector<char> 通常のイテレータの場合:3.13 ms、2.48 ms
  • vector<char> イテレータと境界チェック:2.97 ms、2.53 ms
  • char[]:1.52 ms、1.25 ms

同じラップトップ、Visual C ++ 2010 64ビットコンパイラ:

  • ostringstream:48.6 ms、45.0 ms
  • stringbuf:16.2 ms、16.0 ms
  • vector<char>およびback_inserter:26.3 ms、26.5 ms
  • vector<char> 通常のイテレータの場合:0.87 ms、0.89 ms
  • vector<char> イテレータと境界チェック:0.99 ms、0.99 ms
  • char[]:1.25 ms、1.24 ms

編集:2回すべて実行して、結果の一貫性を確認します。かなり一貫したIMO。

注:私のラップトップでは、ideoneが許可するよりも多くのCPU時間を節約できるため、すべてのメソッドの反復回数を1000に設定しました。この手段ostringstreamvectorだけ最初のパスで行われます再配分は、最終的な結果にはほとんど影響を持っている必要があります。

編集:おっと、vector-with-ordinary-iteratorにバグが見つかりました。イテレータは高度ではなかったため、キャッシュヒットが多すぎました。私はどのようvector<char>に優れているか疑問に思っていましたchar[]。ただし、それほど大きな違いはありませんでしたが、VC ++ 2010 vector<char>よりも高速ですchar[]

結論

出力ストリームのバッファリングには、データが追加されるたびに3つのステップが必要です。

  • 入力ブロックが利用可能なバッファスペースに適合することを確認します。
  • 入力ブロックをコピーします。
  • データ終了ポインタを更新します。

私が投稿した最新のコードスニペット「vector<char>単純なイテレータと境界チェック」は、これを行うだけでなく、追加のスペースを割り当て、受信ブロックが収まらない場合に既存のデータを移動します。クリフォードが指摘したように、ファイルI / Oクラスでのバッファリングはそれを行う必要はなく、現在のバッファーをフラッシュして再利用するだけです。したがって、これは出力をバッファリングするコストの上限になるはずです。そして、それはメモリ内バッファを機能させるために必要なことです。

では、stringbufideoneでは2.5倍遅く、テストすると少なくとも10倍遅くなるのはなぜですか。この単純なマイクロベンチマークでは多態的に使用されていないため、説明はありません。


24
あなたは一度に100万文字を書き込んでいて、それが事前に割り当てられたバッファにコピーするよりも遅いのはなぜだと思いますか?
アノン。

20
@アノン:4百万バイトを一度に4つバッファリングしていますが、はい、なぜそれが遅いのか疑問に思っています。std::ostringstreamバッファサイズを指数関数的に増やすほど賢くない場合std::vector、それは(A)愚かであり、(B)I / Oパフォーマンスについて考える人が考慮すべきことです。とにかく、バッファは再利用され、毎回再割り当てされるわけではありません。またstd::vector、動的に増加するバッファも使用しています。私はここで公平になるようにしています。
Ben Voigt 2010

14
実際にどのようなタスクをベンチマークしようとしていますか?のフォーマット機能を使用せず、ostringstream可能な限り高速なパフォーマンスが必要な場合は、に直接進むことを検討してくださいstringbufostreamクラスは通じ柔軟なバッファの選択(ファイル、文字列など)と一緒に、ロケールを意識書式設定機能を結びつけるために仮定されrdbuf()、その仮想関数インタフェース。書式設定を行わない場合、その余分なレベルの間接参照は、他のアプローチと比較して比例して高価に見えるでしょう。
CBベイリー

5
真実のopの+1。doubleを含むログ情報を出力するときに、からofstreamに移動することでfprintf、次数または絶対値の速度が向上しました。WinXPsp3上のMSVC 2008。iostreamsはただ遅いだけです。
KitsuneYMG 2010

6
ここに委員会サイトでのいくつかのテストがあります:open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
ヨハネスシャウブ-litb

回答:


49

タイトルほど質問の詳細に答えない:C ++パフォーマンスに関する 2006年テクニカルレポートには、IOStreams(p.68)に関する興味深いセクションがあります。あなたの質問に最も関連するのは、セクション6.1.2(「実行速度」)です。

IOStreams処理の特定の側面は複数のファセットに分散されているため、標準では非効率的な実装が義務付けられているようです。しかし、そうではありません。何らかの前処理を使用することで、作業の多くを回避できます。通常使用されるよりも少しスマートなリンカーを使用すると、これらの非効率の一部を取り除くことができます。これについては、§6.2.3および§6.2.5で説明します。

このレポートは2006年に作成されたため、推奨事項の多くが現在のコンパイラに組み込まれていることを期待していますが、おそらくそうではありません。

あなたが言及するように、ファセットは機能しないかもしれませんwrite()(しかし、私はそれを盲目的に想定しません)。それでは何が特徴ですか?ostringstreamGCCでコンパイルされたコードでGProfを実行すると、次の内訳が得られます。

  • 44.23%で std::basic_streambuf<char>::xsputn(char const*, int)
  • 34.62%で std::ostream::write(char const*, int)
  • 12.50%で main
  • 6.73%で std::ostream::sentry::sentry(std::ostream&)
  • 0.96%で std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0.96%で std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0.00% std::fpos<int>::fpos(long long)

したがって、時間の大部分はで費やされxsputn、最終的にはstd::copy()、カーソル位置とバッファーの多数のチェックと更新の後に呼び出されます(c++\bits\streambuf.tcc詳細を確認してください)。

これについての私の見方は、あなたは最悪の状況に焦点を当ててきたということです。適度に大きなデータのチャンクを扱っている場合、実行されるすべてのチェックは、行われる作業全体のごく一部です。しかし、コードは一度に4バイトでデータをシフトし、毎回すべての追加コストが発生します。明らかに、実際の状況ではそうすることを避けwriteます。1つのintで1m回ではなく、1m intの配列で呼び出された場合のペナルティが無視できる程度であることを考慮してください。そして、実際の状況では、IOStreamsの重要な機能、つまりそのメモリセーフおよびタイプセーフデザインに本当に感謝します。このような利点には代償が伴い、これらのコストが実行時間を支配するようにするテストを作成しました。


iostreamのフォーマットされた挿入/抽出のパフォーマンスに関する将来の質問については、おそらくすぐに尋ねる素晴らしい情報のようです。ただし、に関連する側面はないと思いますostream::write()
Ben Voigt 2010

4
プロファイリングの+1(これは私が想定しているLinuxマシンですか?)。ただし、実際には一度に4バイトを追加しています(実際sizeof iには、テストしているすべてのコンパイラは4バイトですint)。そして、それは私にとってそれほど現実的ではないように思わxsputnれますstream << "VAR: " << var.x << ", " << var.y << endl;
Ben Voigt

39
@beldaz:xsputn5回しか呼び出さない「典型的な」コード例は、1000万行のファイルを書き込むループ内にある可能性があります。大きなチャンクでデータをiostreamに渡すことは、私のベンチマークコードよりも実際のシナリオよりもはるかに少なくなります。最小限の呼び出しでバッファストリームに書き込む必要があるのはなぜですか?独自のバッファリングを行う必要がある場合、とにかくiostreamのポイントは何ですか?バイナリデータでは、自分でバッファリングするオプションがあります。テキストファイルに数百万の数値を書き込むとき、バルクオプションは存在しないのでoperator <<、それぞれを呼び出す必要があります。
Ben Voigt

1
@beldaz:簡単な計算でI / Oが支配的になる時期を推定できます。現在のコンシューマーグレードのハードディスクに一般的な90 MB / sの平均書き込み速度では、4MBバッファーのフラッシュには45ミリ秒未満かかります(OS書き込みキャッシュのため、スループット、待ち時間は重要ではありません)。内側のループの実行にバッファの充填にそれよりも時間がかかる場合、CPUが制限要因になります。内部ループの実行速度が速い場合は、I / Oが制限要因になるか、少なくとも実際の作業を行うためのCPU時間が残っています。
Ben Voigt

5
もちろん、これはiostreamの使用が必ずしも遅いプログラムを意味するという意味ではありません。I / Oがプログラムの非常に小さな部分である場合、パフォーマンスの低いI / Oライブラリを使用しても、全体的な影響はそれほど大きくありません。しかし、問題になるほど頻繁に呼び出されないことは、優れたパフォーマンスと同じではなく、I / Oが多いアプリケーションでは問題になります。
Ben Voigt

27

私は、Visual Studioのユーザーにかなりがっかりしています。

  • のVisual Studio実装でostreamは、sentryオブジェクト(標準でstreambuf必要)が(必須ではない)を保護するクリティカルセクションに入ります。これはオプションのようには見えないため、同期の必要がない単一のスレッドによって使用されるローカルストリームの場合でも、スレッド同期のコストを負担します。

これはostringstream、メッセージのフォーマットに使用するコードをかなりひどく傷つけます。をstringbuf直接使用するとsentry、の使用が回避されますが、フォーマットされた挿入演算子はで直接機能しませんstreambuf。Visual C ++ 2010の場合、クリティカルセクションはostringstream::write、基になるstringbuf::sputn呼び出しに対して3倍遅くなります。

newlibbeldazのプロファイラーデータを見ると、gcc sentryはこのように奇妙なことを何もしていないことが明らかなようです。 ostringstream::writegccの下では、の約50%しかかかりませんがstringbuf::sputn、それstringbuf自体はVC ++の下よりもはるかに遅くなります。また、vector<char>VC ++と同じマージンではありませんが、どちらもI / Oバッファリングにforを使用する場合と比べて非常に好ましくありません。


この情報はまだ最新ですか?AFAIK、GCCに同梱されているC ++ 11実装は、この「クレイジー」ロックを実行します。確かに、VS2010もまだそれを行います。誰かがこの動作を明確にできますか?「これは必要ありません」がまだC ++ 11に当てはまる場合?
mloskot

2
@mloskot:スレッドセーフの要件はありませんsentry...「クラスセントリーは、例外セーフプレフィックスおよびサフィックス操作の実行を担当するクラスを定義しています。」また、「監視ユニットのコンストラクタとデストラクタは、実装に依存する追加の操作を実行することもできます。」また、C ++委員会がそのような無駄な要件を承認することは決してないだろうというC ++の原則から、「使用しないものについては支払いをしない」と推測することもできます。しかし、iostreamスレッドの安全性について質問してください。
Ben Voigt

8

表示される問題は、すべてwrite()の呼び出しに関するオーバーヘッドにあります。追加する抽象化の各レベル(char []-> vector-> string-> ostringstream)は、関数呼び出し/戻り、および他のハウスキーピングガフをいくつか追加します。

ideoneの例を2つ変更して、一度に10整数を記述しました。ostringstreamの時間は53から6ミリ秒(ほぼ10倍の改善)になりましたが、charループは改善されました(3.7から1.5)-有用ですが、2倍にすぎません。

パフォーマンスが気になる場合は、ジョブに適したツールを選択する必要があります。ostringstreamは便利で柔軟性がありますが、意図したとおりに使用するとペナルティがあります。char []は難しい作業ですが、パフォーマンスが大幅に向上する可能性があります(gccはおそらくmemcpysもインライン化することを忘れないでください)。

要するに、ostringstreamは壊れていませんが、金属に近づくほどコードの実行が速くなります。アセンブラは、一部の人にとってはまだ利点があります。


8
それは何をしostringstream::write()なければならvector::push_back()ないのですか?どちらかと言えば、4つの個別の要素の代わりにブロックを渡されているため、より高速になるはずです。追加の機能を提供しないostringstreamよりも遅い場合はstd::vector、ええ、私はそれが壊れていると呼びます。
Ben Voigt 2010

1
@Ben Voigt:逆に、その何かのベクトルは、ostringstreamを実行する必要があります。この場合、ベクトルをより高性能にするために実行する必要はありません。ベクトルはメモリ内で連続していることが保証されていますが、ostringstreamはそうではありません。Vectorはパフォーマンスが高くなるように設計されたクラスの1つですが、ostringstreamはそうではありません。
Dragontamer5788 2010

2
@Ben Voigt:のstringbuf直接stringbufインターフェイスを使用しても、すべての関数呼び出しが削除されるわけではありません。そのパブリックインターフェイスは、基本クラスのパブリック非仮想関数で構成されているため、派生クラスの保護された仮想関数にディスパッチされます。
CBベイリー

2
@Charles:適切なコンパイラーでは、パブリック関数呼び出しが動的型がコンパイラーに認識されているコンテキストにインライン化されるため、間接参照を削除し、それらの呼び出しをインライン化することもできます。
Ben Voigt

6
@Roddy:これは、すべてのコンパイルユニットに表示される、すべてインラインテンプレートコードであると考えるべきです。しかし、それは実装によって異なる可能性があると思います。確かに私は議論中のsputn呼び出し、virtual protectedを呼び出すパブリック関数xsputnがインライン化されることを期待します。xsputnがインライン化されていなくても、コンパイラーはインライン化中にsputnxsputn必要な正確なオーバーライドを決定し、vtableを経由せずに直接呼び出しを生成できます。
Ben Voigt

1

パフォーマンスを向上させるには、使用しているコンテナがどのように機能するかを理解する必要があります。char []配列の例では、必要なサイズの配列が事前に割り当てられています。vectorとostringstreamの例では、オブジェクトが大きくなるにつれて、オブジェクトに繰り返し割り当てと再割り当てを強制し、データを何度もコピーします。

std :: vectorを使用すると、char配列のようにベクターのサイズを最終的なサイズに初期化することで簡単に解決できます。代わりに、サイズをゼロに変更することで、パフォーマンスを不当に低下させます。それは公平な比較ではありません。

ostringstreamに関しては、スペースを事前に割り当てることはできません。これは不適切な使用であることをお勧めします。クラスのユーティリティは単純なchar配列よりもはるかに優れていますが、そのユーティリティが必要ない場合は使用しないでください。いずれにしてもオーバーヘッドが発生するためです。代わりに、データを文字列にフォーマットするために使用する必要があります。C ++は幅広いコンテナーを提供しており、ostringstramはこの目的に最も適していないものの1つです。

vectorとostringstreamの場合、バッファーオーバーランから保護されますが、char配列では保護されません。その保護は無料ではありません。


1
割り当てはostringstreamの問題ではないようです。彼は後続の反復のためにゼロに戻ろうとします。切り捨てなし。また、試してみostringstream.str.reserve(4000000)ましたが違いはありませんでした。
Roddy

私はと考えてostringstreamダミーの文字列を渡すことによって、あなたはできる「予備」、すなわち:ostringstream str(string(1000000 * sizeof(int), '\0'));ではvectorresizeそれが必要な場合は任意のスペースを解放していない、それだけで展開します。
Nim

1
「ベクトル..バッファオーバーランからの保護」。よくある誤解- vector[]デフォルトでは、オペレーターは通常、境界エラーについてチェックされません。vector.at()しかしです。
Roddy

2
vector<T>::resize(0)通常、メモリの再割り当ては行われません
Niki Yoshiuchi、2010

2
@Roddy:を使用operator[]していませんが、push_back()(を使用してback_inserter)、オーバーフローのテストを確実に行っています。を使用しない別のバージョンを追加しましたpush_back
Ben Voigt 2010
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.