次のようなc ++コードがたくさんあります。
for( const_iterator it = list.begin(),
const_iterator ite = list.end();
it != ite; ++it)
より簡潔なバージョンとは対照的に:
for( const_iterator it = list.begin();
it != list.end(); ++it)
これらの2つの規則の間に速度の違いはありますか? list.end()は1回しか呼び出されないため、単純に最初の方が少し速くなります。しかし、イテレータはconstであるため、コンパイラはこのテストをループから引き出し、両方の同等のアセンブリを生成するようです。
レコードについては、C++標準ではbegin()
およびend()
onanyを呼び出すことを義務付けていることに言及します。コンテナの種類(vector
、list
、map
など)一定の時間しかかかりません。実際には、最適化をオンにしてコンパイルすると、これらの呼び出しはほぼ確実に単一のポインター比較にインライン化されます。
この保証は、標準の第23章に記載されているコンテナであるという正式な要件を実際に満たしていない追加のベンダー提供の「コンテナ」には必ずしも当てはまらないことに注意してください(たとえば、単一リンクリストslist
)。
最初のものはおそらくほぼ常に高速ですが、これが違いを生むと思う場合は、常にプロファイルを最初にプロファイルして、どれがより速く、どれだけ。
コンパイラーはおそらくどちらの場合でもend()
への呼び出しをインライン化できますが、end()
が十分に複雑な場合は、インライン化しないことを選択できます。ただし、重要な最適化は、コンパイラが loop-invariant code motion を実行できるかどうかです。ほとんどの場合、コンパイラはend()
の値がループの反復中に変化しないことを確信できないと仮定します。その場合、end()
各反復の後。
最も簡潔で読みやすいオプションを選択します。コンパイラとそれが実行する可能性のある最適化を2番目に推測しようとしないでください。コードの大部分は全体的なパフォーマンスにまったく影響しないため、これがコードのパフォーマンスクリティカルセクションにある場合にのみ、プロファイルを作成し、適切に効率的なソース表現を選択する必要があります。
例を参照すると、最初のバージョンはend()
イテレーターのcopyを作成し、イテレーターオブジェクトのコピーコンストラクターで実行されるコードを呼び出します。 STLコンテナには通常、インラインend()
関数が含まれているため、2番目のバージョンを支援しようとしていない場合でも、コンパイラには2番目のバージョンを最適化する機会がたくさんあります。どれがベストですか?それらを測定します。
この例を考えてみましょう:
for (const_iterator it = list.begin(); it != list.end(); ++list)
{
if (moonFull())
it = insert_stuff(list);
else
it = erase_stuff(list);
}
この場合、ループ内でlist.end()を呼び出す必要がありますが、コンパイラーはそれを最適化しません。
コンパイラーがend()が常に同じ値を返すことを証明できる他のケースでは、最適化が行われます。
STLコンテナについて話している場合、プログラミングロジックに複数のend()呼び出しが必要ない場合、優れたコンパイラは複数のend()呼び出しを最適化して排除できると思います。ただし、カスタムコンテナがあり、end()の実装が同じ翻訳単位にない場合は、リンク時に最適化を行う必要があります。リンク時間の最適化についてはほとんど知りませんが、ほとんどのリンカーはそのような最適化を行わないでしょう。
最初のバージョンをより簡潔にし、両方を最大限に活用できます。
for( const_iterator it = list.begin(), ite = list.end();
it != ite; ++it)
追伸反復子はconstではなく、const参照の反復子です。大きな違いがあります。
コンパイラーは、2番目のものを最初のものに最適化できる可能性がありますが、それは2つが同等であると想定しています。もう少し問題の多い問題は、エイリアスが発生する可能性があるため、コンパイラがエンドイテレータが一定であると推定できない場合があることです。ただし、end()の呼び出しがインライン化されていると仮定すると、違いは単なるメモリ負荷です。
これは、オプティマイザーが有効になっていることを前提としていることに注意してください。デバッグビルドでよく行われるように、オプティマイザーが有効になっていない場合、2番目の定式化には、さらにN-1個の関数呼び出しが含まれます。 Visual C++の現在のバージョンでは、関数プロローグ/エピログチェックおよびより重いデバッグイテレーターにより、デバッグビルドでも追加のヒットが発生します。したがって、STLの重いコードでは、最初のケースにデフォルト設定すると、デバッグビルドでコードが不均衡に遅くなるのを防ぐことができます。
他の人が指摘しているように、ループ内での挿入と削除は可能ですが、このスタイルのループではそうは思えません。一つには、ノードベースのコンテナ-リスト、セット、マップ-は、どちらの操作でもend()を無効にしないでください。第二に、無効化の問題を回避するために、反復子の増分をループ内で頻繁に移動する必要があります。
//リストを仮定-vector iterator it(c.begin())、end(c.end()); while(it != end){ if(should_remove(* it)) it = c.erase(it); else ++ it; }
したがって、ループ中にmutate-during-loopの理由でend()を呼び出すと主張し、疑わしいループヘッダーに++ itがまだあるループを考えます。
ああ、人々は推測をしているようです。デバッガでコードを開くと、begin()、end()などの呼び出しがすべて最適化されていることがわかります。バージョン1を使用する必要はありません。VisualC++コンパイラfulloptでテスト済みです。
ストレス状態でそれをサンプリングし、**このコードに頻繁にいるかどうかを確認してください***。
そうでない場合、それは問題ではありません。
もしそうなら、逆アセンブリを見るか、それをシングルステップします。
これは、どちらが速いかを判断する方法です。
これらのイテレータに注意する必要があります。
Niceマシンコードに最適化される場合がありますが、そうでない場合が多く、時間の浪費になります。
**(「イン」とは、実際にその中にある、またはそこから呼び出されることを意味します。)
***(「頻繁に」は時間のかなりの割合を意味します。)
追加:コードが1秒間に何回実行されるかだけを見ないでください。 1秒間に1,000回であり、それでも時間の1%未満を使用している可能性があります。
どれだけ時間がかかるかを計らないでください。 1ミリ秒かかる可能性がありますが、それでも時間の1%未満しか使用していません。
より良いアイデアを得るために2つを掛けることができますが、それはそれらがあまりにも歪んでいない場合にのみ機能します。
コールスタックのサンプリング は、重要な時間の割合が十分に高いかどうかを示します。
私は常に最初のものを好んだ。インライン関数、コンパイラーの最適化、比較的小さいコンテナーサイズ(私の場合は通常最大20-25アイテム)ですが、実際にはパフォーマンスに関して大きな違いはありません。
const_iterator it = list.begin();
const_iterator endIt = list.end();
for(; it != endIt ; ++it)
{//do something
}
しかし、最近、可能な限り std :: for_each を使用しています。コードを他の2つより読みやすくするのに役立つ、最適化されたループ。
std::for_each(list.begin(), list.end(), Functor());
ループを使用するのは、std::for_each
は使用できません。 (例:std::for_each
では、例外がスローされない限り、ループを中断できません。
理論的には、コンパイラは2番目のバージョンを最初のバージョンに最適化することができます(コンテナーがループ中に変化しないことを前提としています)。
実際には、タイムクリティカルなコードをプロファイリングするときに、コンパイラーがループ条件から不変の計算を完全に引き上げることに失敗した同様のケースをいくつか見つけました。そのため、ほとんどの場合、少し簡潔なバージョンでも問題ありませんが、パフォーマンスが本当に心配な場合は、コンパイラが賢明なことを行うことに依存しません。