web-dev-qa-db-ja.com

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

C++標準ライブラリのiostreamのパフォーマンスの低下について言及するたびに、不信の波に出くわします。しかし、私はiostreamライブラリコード(完全なコンパイラー最適化)に費やされた大量の時間を示すプロファイラーの結果があり、iostreamからOS固有のI/O APIとカスタムバッファー管理に切り替えると、桁違いに改善されます。

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

ベンチマーク

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

ostringstreamおよびstringbufバージョンは、非常に遅いため、実行される反復回数が少ないことに注意してください。

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

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

他のシステムのベンチマークや、一般的な実装(gccのlibc ++、Visual C++、Intel C++など)のコメントや、標準によってどの程度のオーバーヘッドが必要かについてのコメントを見るといいでしょう。

このテストの理由

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

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

これらはすべて、外側(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ミリ秒
  • char[]:3.7ミリ秒

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

  • ostringstream:73.4ミリ秒、71.6ミリ秒
  • stringbuf:21.7ミリ秒、21.3ミリ秒
  • vector<char>およびback_inserter:34.6ミリ秒、34.4ミリ秒
  • 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ミリ秒、1.57ミリ秒

Visual C++ 2010 x86、プロファイルに基づく最適化cl /Ox /EHsc /GL /clink /ltcg:pgi、実行、link /ltcg:pgo、測定:

  • ostringstream:61.2ミリ秒、60.5ミリ秒
  • vector<char>通常のイテレータを使用:1.04 ms、1.03 ms

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

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

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

  • ostringstream:88.7 ms、87.6 ms
  • stringbuf:23.3ミリ秒、23.4ミリ秒
  • vector<char>およびback_inserter:26.1ミリ秒、24.5ミリ秒
  • 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ミリ秒、16.0ミリ秒
  • 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ミリ秒、1.24ミリ秒

編集:結果がどの程度一貫しているかを確認するために、すべて2回実行しました。かなり一貫したIMO。

注:私のラップトップでは、ideoneが許可するよりも多くのCPU時間を使用できるため、すべてのメソッドの反復回数を1000に設定します。つまり、最初のパスでのみ実行されるostringstreamおよびvector再割り当ては、最終結果にほとんど影響を与えないはずです。

編集:おっと、vector- with-ordinary-iteratorにバグが見つかりました。イテレータは進んでいなかったため、キャッシュヒットが多すぎました。 vector<char>がどのようにchar[]を上回っていたのかと思っていました。ただし、VC++ 2010では、vector<char>char[]よりも高速です。

結論

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

  • 着信ブロックが使用可能なバッファースペースに収まることを確認します。
  • 着信ブロックをコピーします。
  • データの終わりポインターを更新します。

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

それでは、なぜideoneではstringbufが2.5倍遅く、テストすると少なくとも10倍遅くなるのでしょうか?この単純なベンチマークでは多態的に使用されていないため、説明しません。

193
Ben Voigt

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

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

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

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

  • std::basic_streambuf<char>::xsputn(char const*, int)の44.23%
  • std::ostream::write(char const*, int)の34.62%
  • mainで12.50%
  • std::ostream::sentry::sentry(std::ostream&)の6.73%
  • 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.96%
  • std::fpos<int>::fpos(long long)の0.00%

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

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

47
beldaz

私はそこにいるVisual Studioユーザーに失望しています。

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

これは、ostringstreamを使用するメッセージをかなりひどくフォーマットするコードを傷つけます。 stringbufを直接使用すると、sentryの使用を回避できますが、フォーマットされた挿入演算子はstreambufsで直接動作できません。 Visual C++ 2010の場合、重要なセクションは、ostringstream::writeを3倍の速度で、対のstringbuf::sputn呼び出しよりも遅くしています。

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

27
Ben Voigt

表示される問題はすべて、write()の各呼び出しのオーバーヘッドにあります。追加する抽象化の各レベル(char []->ベクトル->文字列-> ostringstream)は、関数呼び出し/戻り値とその他のハウスキーピングガフをさらに追加します。

Ideoneの2つの例を修正して、一度に10個のintを記述しました。 ostringstreamの時間は53ミリ秒から6ミリ秒(ほぼ10倍の改善)になりましたが、charループは改善されました(3.7から1.5)-有用ですが、2分の1だけです。

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

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

8
Roddy

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

Std :: vectorを使用すると、char配列と同じようにベクトルのサイズを最終サイズに初期化することで簡単に解決できます。代わりに、ゼロにサイズ変更することにより、パフォーマンスをかなり不自由にします!それはほとんど公平な比較ではありません。

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

ベクトルとostringstreamの場合、バッファオーバーランから保護されますが、char配列では保護されず、その保護は無料では提供されません。

1
Clifford