5

アプリケーションのパフォーマンスを向上させるために、開発段階でループの最適化手法を検討する必要があります。

単純な を反復処理するいくつかの異なる方法を紹介したいと思いますstd::vector<uint32_t> v

  1. 索引付きの最適化されていないループ:

    uint64_t sum = 0;
    for (unsigned int i = 0; i < v.size(); i++)
        sum += v[i];
    
  2. イテレータを含む最適化されていないループ:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it;
    for (it = v.begin(); it != v.end(); it++)
        sum += *it;
    
  3. キャッシュされたstd::vector::end反復子:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it, end(v.end());
    for (it = v.begin(); it != end; it++)
        sum += *it;
    
  4. プリインクリメント イテレータ:

    uint64_t sum = 0;
    std::vector<uint32_t>::const_iterator it, end(v.end());
    for (it = v.begin(); it != end; ++it)
        sum += *it;
    
  5. 範囲ベースのループ:

    uint64_t sum = 0;
    for (auto const &x : v)
        sum += x;
    

C++ でループを構築する方法は他にもあります。たとえば、、などを使用しstd::for_eachBOOST_FOREACH...

あなたの意見では、パフォーマンスを向上させるための最良のアプローチはどれですか?またその理由は何ですか?

さらに、パフォーマンスが重要なアプリケーションでは、ループをアンロールすると便利な場合があります。繰り返しになりますが、どのアプローチをお勧めしますか?

4

5 に答える 5

2

最新のコンパイラは、上記のアプローチに対して同じアセンブリを生成する可能性が非常に高いです。確認するには、(最適化を有効にした後) 実際のアセンブリを確認する必要があります。

ループの速度が気になるときは、アルゴリズムが本当に最適かどうかをよく考える必要があります。そうであると確信している場合は、データ構造の基礎となる実装について考える (そして利用する) 必要があります。 std::vectorは下で配列を使用し、コンパイラと関数内の他のコードによっては、ポインターのエイリアシングにより、コンパイラがコードを完全に最適化できない場合があります。

ポインターのエイリアシングに関する情報はかなりありますが ( What is the strict aliasing rule?を含む)、Mike Acton はポインターのエイリアシングに関するすばらしい情報を提供しています。

restrictキーワード ( What does the restrict keyword mean in C++?または再びMike Actonを参照) は、長年にわたりコンパイラ拡張機能を通じて利用可能であり、C99 で体系化されています (現在、C++ のコンパイラ拡張機能としてのみ利用可能です)。 . コードでこれを使用する方法ははるかに C に似ていますが、少なくとも指定した例では、コンパイラがループをより適切に最適化できるようになる場合があります。

uint64_t sum = 0;
uint32_t *restrict velt = &v[0];
uint32_t *restrict vend = velt + v.size();
while(velt < vend) {
  sum += *velt;
  velt++;
}

ただし、これが違いをもたらすかどうかを確認するには、実際の実際の問題に対するさまざまなアプローチをプロファイルし、生成された基になるアセンブリを確認する必要があります。単純なデータ型を合計している場合、これが役立つ場合があります。ループ内でインライン化できない関数を呼び出すなど、より複雑なことをしている場合は、まったく違いはありません。

于 2013-08-13T16:39:22.833 に答える
2

私は、あなたが時期尚早のマイクロ最適化の弊害を十分に認識しており、プロファイリングなどによってコード内のホットスポットを特定していることを前提としています。また、速度に関するパフォーマンスのみを気にしていると仮定しています。つまり、結果のコードのサイズやメモリの使用量を深く気にする必要はありません。

end()提供されたコード スニペットは、キャッシュされた反復子を除いて、ほとんど同じ結果をもたらします。できる限りキャッシングとインライン化を行う以外に、パフォーマンスの大幅な向上を実現するために上記のループの構造を微調整するためにできることはあまりありません。

クリティカル パスでパフォーマンスの高いコードを記述するには、何よりもまず、ジョブに最適なアルゴリズムを選択する必要があります。パフォーマンスに問題がある場合は、まずアルゴリズムをよく見てください。一般に、コンパイラは、記述したコードのマイクロ最適化において、期待以上に優れた仕事をします。

とはいえ、コンパイラを少しだけ支援するためにできることがいくつかあります。

  • 可能な限りすべてをキャッシュする
  • 特にループ内では、小さな割り当てを最小限に抑える
  • できるだけ多くのものを作ってくださいconst。これにより、コンパイラにマイクロ最適化の機会が追加されます。
  • ツールチェーンをよく学び、その知識を活用する
  • アーキテクチャをよく学び、その知識を活用する
  • アセンブリ コードを読み、コンパイラからのアセンブリ出力を調べる方法を学ぶ

ツールチェーンとアーキテクチャを学習することで、最大のメリットが得られます。たとえば、GCC には、ループ展開など、パフォーマンスを向上させるために有効にできる多くのオプションがあります。ここを参照してください。データセットを反復する場合、各アイテムをキャッシュ ラインのサイズに合わせておくと便利なことがよくあります。現代のアーキテクチャでは、これは多くの場合 64 バイトを意味しますが、アーキテクチャについて学習してください。

これは、インテル環境でパフォーマンスの高い C++ を作成するための優れたガイドです。

アーキテクチャとツールチェーンを理解すると、最初に選択したアルゴリズムが現実の世界では最適ではないことに気付く場合があります。新しいデータに直面しても、変化に対してオープンであること。

于 2013-08-13T16:33:56.660 に答える
0

最適化するための最も直接的な情報がコンパイラーに提供されるため、デフォルトでは範囲ベースを使用します (コンパイラーは、たとえば、終了イテレーターをキャッシュできることを認識しています)。次にプロファイルを作成し、重大なボトルネックを特定した場合にのみさらに最適化します。これらのさまざまなループ バリアントがパフォーマンスに大きな違いをもたらす現実の状況はほとんどありません。コンパイラはループの最適化に非常に優れており、最適化の取り組みを他の場所に集中させる必要がある可能性がはるかに高くなります (より良いアルゴリズムを選択するか、ループ本体の最適化に集中するなど)。

于 2013-10-25T23:48:10.907 に答える