web-dev-qa-db-ja.com

std :: vectorを反復する最も効果的な方法は何ですか?なぜですか?

時空の複雑さに関して、次のうちどれがstd :: vectorを反復するための最良の方法であり、なぜですか?

方法1:

for(std::vector<T>::iterator it = v.begin(); it != v.end(); ++it) {
    /* std::cout << *it; ... */
}

方法2:

for(std::vector<int>::size_type i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

方法3:

for(size_t i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

方法4:

for(auto const& value: a) {
     /* std::cout << value; ... */
41
Suhasis

まず、Way 2Wayは、事実上すべての標準ライブラリ実装で同じです。

それとは別に、投稿したオプションはほとんど同じです。唯一の注目すべき違いは、ウェイ1およびウェイ2/では、コンパイラーを使用してv.end()およびv.size() アウト。その仮定が正しい場合、ループ間にパフォーマンスの違いはありません。

そうでない場合、Way 4が最も効率的です。 forループに基づく範囲がどのように拡張されるかを思い出してください

_{
   auto && __range = range_expression ;
   auto __begin = begin_expr ;
   auto __end = end_expr ;
   for ( ; __begin != __end; ++__begin) {
      range_declaration = *__begin;
      loop_statement
   }
}
_

ここで重要なのは、これにより_end_expr_が1回だけ評価されることが保証されることです。また、範囲ベースのforループが最も効率的な反復になるため、イテレータの逆参照の処理方法を変更しないでください。

_for (auto value: a) { /* ... */ }
_

これにより、ベクターの各要素がループ変数valueにコピーされます。これは、ベクターの要素のサイズによっては、for (const auto& value : a)よりも低速になる可能性があります。

C++ 17の並列アルゴリズム機能を使用すると、

_#include <algorithm>
#include <execution>

std::for_each(std::par_unseq, a.cbegin(), a.cend(),
   [](const auto& e) { /* do stuff... */ });
_

しかし、これが通常のループよりも高速であるかどうかは、状況によって異なります。

37
lubgr

インデックス/キーよりもイテレータを優先します。

vectorまたはarrayの場合、どちらの形式にも違いはありません1、他のコンテナに入るのは良い習慣です。

1 もちろん、インデックスによるアクセスに.at()の代わりに[]を使用する限り。


エンドバウンドを覚えてください。

各反復で終了境界を再計算することは、次の2つの理由で非効率的です。

  • 一般に、ローカル変数はエイリアス化されないため、オプティマイザにより使いやすくなります。
  • ベクトル以外のコンテナーの場合:終了/サイズの計算は少しコストがかかる可能性があります。

あなたはワンライナーとしてそうすることができます:

for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }

(これは、一度に1つの変数を宣言することに対する一般的な禁止の例外です。)


For-eachループ形式を使用します。

For-eachループフォームは自動的に次のようになります。

  • イテレータを使用します。
  • エンドバウンドを覚えてください。

したがって:

for (/*...*/ value : vec) { ... }

組み込み型を値で、その他の型を参照で取得します。

要素を値で取得することと、要素を参照で取得することの間には、明白ではないトレードオフがあります。

  • 要素を参照で取得すると、コピーが回避され、コストのかかる操作になる可能性があります。
  • 要素を値で取得することで、オプティマイザが使いやすくなります1

極端な場合、選択は明白なはずです。

  • 組み込み型(intstd::int64_tvoid*、...)は値で取得する必要があります。
  • 潜在的に割り当てられる型(std::string、...)は、参照する必要があります。

途中、または一般的なコードに直面したときは、参照から始めることをお勧めします。最後のサイクルを絞り出そうとするよりも、パフォーマンスの崖を避けた方がいいです。

したがって、一般的な形式は次のとおりです。

for (auto& element : vec) { ... }

そして、あなたが組み込みのものを扱っているなら:

for (int element : vec) { ... }

1 これは最適化の一般的な原則です。実際には、オプティマイザはローカル変数のすべての潜在的なエイリアス(またはその不在)を知っているため、ローカル変数はポインタ/参照よりも使いやすくなっています。

12
Matthieu M.

lubgr 's answer への追加:

問題のコードをボトルネックとしてプロファイリングして検出しない限り、少なくともこのレベルのコードでは、効率(おそらく「有効性」の代わりに意味する)は最初の懸念事項ではありません。さらに重要なのは、コードの可読性と保守性です。したがって、最もよく読み取るループバリアントを選択する必要があります。これは通常、方法4です。

インデックスは、1より大きいステップがある場合に役立ちます(ただし、必要な場合は...):

for(size_t i = 0; i < v.size(); i += 2) { ... }

+= 2それ自体はイテレータでも有効です。終了位置を超えてインクリメントするため、ベクトルのサイズが奇数の場合、ループの終了時に未定義の動作が発生する危険があります。 (一般的には、nをインクリメントすると、サイズがnの正確な倍数でない場合、UBが取得されます。これをキャッチするには追加のコードが必要ですが、インデックスバリアントは必要ありません...

10
Aconcagua

怠惰な答え:複雑さは同等です。

  • すべてのソリューションの時間の複雑さはΘ(n)です。
  • すべてのソリューションのスペースの複雑さはΘ(1)です。

さまざまなソリューションに含まれる定数要素は、実装の詳細です。数値が必要な場合は、特定のターゲットシステムでさまざまなソリューションのベンチマークを行うのが最善の方法です。

v.size() rspを保存すると役立ちます。 v.end()、ただしこれらは通常インライン化されるため、そのような最適化は必要ない場合があります。または 自動的に実行されます

v.size()をメモせずに)インデックスを作成することが、(Push_back()を使用して)要素を追加する可能性のあるループ本体を正しく処理する唯一の方法であることに注意してください。ただし、ほとんどのユースケースでは、この追加の柔軟性は必要ありません。

3
Arne Vogel

メソッド4、std :: for_each(本当に必要な場合)、またはメソッド5/6を優先します。

void method5(std::vector<float>& v) {
    for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it) {
        *it *= *it; 
    }
}
void method6(std::vector<float>& v) {
    auto ptr = v.data();
    for(std::size_t i = 0, n = v.size(); i != n; i++) {
        ptr[i] *= ptr[i]; 
    }
}

最初の3つのメソッドは、ポインターのエイリアスの問題(以前の回答で暗示されているように)の影響を受ける可能性がありますが、すべて同じように悪いです。別のスレッドmayがベクターにアクセスしている可能性があることを考えると、ほとんどのコンパイラーはそれを安全に再生し、[] end()とsize()を再評価します各反復で。これにより、すべてのSIMD最適化が妨げられます。

ここで証明を見ることができます:

https://godbolt.org/z/Bchhm

4/5/6だけがvmulps SIMD命令を使用していることに気づくでしょう。1/ 2/3は非SIMD vmulss命令しか使用していません。

注:godboltリンクでVC++を使用していますが、これは問題を適切に示しているためです。同じ問題がgcc/clangでも発生しますが、godboltでそれをデモンストレーションするのは容易ではありません。通常、これが発生していることを確認するには、DSOを分解する必要があります。

1
robthebloke

それはあなたが「効果的」で何を意味するかに大きく依存します。

他の回答では効率について言及していますが、C++コードの(IMO)最も重要な目的に焦点を当てます:intent他のプログラマーへ¹。

この観点から、方法4は明らかに最も効果的です。読み取る文字数が少ないためだけでなく、主に認識的負荷が少ないため:境界またはステップサイズが正しいかどうかを確認する必要はありません異常、ループ反復変数(iまたはit)が他の場所で使用または変更されているかどうか、タイプミスまたはfor (auto i = 0u; i < v1.size(); ++i) { std::cout << v2[i]; }などのコピー/貼り付けエラーがあるかどうか、または数十他の可能性の。

簡単なクイズ:与えられたstd::vector<int> v1, v2, v3;、次のループのうちいくつが正しいですか?

for (auto it = v1.cbegin();  it != v1.end();  ++it)
{
    std::cout << v1[i];
}

for (auto i = 0u;  i < v2.size();  ++i)
{
    std::cout << v1[i];
}

for (auto const i: v3)
{
    std::cout << i;
}

ループ制御をできるだけ明確に表現することで、開発者の心は実装の詳細に煩わされることなく、高レベルのロジックをより深く理解できます。結局のところ、これが最初にC++を使用している理由です。


clear明確にするために、私がコードを書いているとき、私は最も重要な「他のプログラマー」をFuture Meであると考え、「誰がこのゴミを書いたのか? "...

1
Toby Speight

完全を期すために、ループがベクターのサイズを変更する可能性があることを述べておきたいと思います。

_std::vector<int> v = get_some_data();
for (std::size_t i=0; i<v.size(); ++i)
{
    int x = some_function(v[i]);
    if(x) v.Push_back(x);
}
_

このような例では、インデックスを使用する必要があり、反復ごとにv.size()を再評価する必要があります。

同じことを範囲ベースのforループまたはイテレーターで行うと、ベクターに新しい要素を追加するとイテレーターが無効になる可能性があるため、最終的にndefined behaviorとなる可能性があります。

ところで、私はwhile- loopsよりもそのような場合にfor- loopsを使用することを好みますが、それは別の話です。

1
Handy999

リストしたすべての方法は、同じ時間の複雑さと同じ空間の複雑さを持っています(そこに驚きはありません)。

for(auto& value : v)構文を使用すると、他のメソッドでは、コンパイラーがテストを実行するたびにメモリからv.size()およびv.end()を再ロードする可能性があるため、わずかに効率的ですfor(auto& value : v)の場合、これは発生しません(begin()およびend()イテレータを1回だけロードします)。

ここで、各メソッドによって生成されたアセンブリの比較を観察できます。 https://godbolt.org/z/LnJF6p

少しおかしいことに、コンパイラーはmethod3jmp命令としてmethod2に実装します。

0

コンテナの最後が一度だけ評価されるため、理論的にはより高速な最後の1つを除いて、複雑さはすべて同じです。

最後の1つは、読み書きにも最適ですが、インデックスを提供しないという欠点があります(これは非常に重要です)。

しかし、あなたは私が良い代替だと思うものを無視しています(それは私がインデックスを必要とし、for (auto& x : v) {...}を使用できない場合の私の好ましいものです):

for (int i=0,n=v.size(); i<n; i++) {
    ... use v[i] ...
}

size_tではなくintを使用したこと、および終了は1回だけ計算され、ローカル変数として本文でも使用できることに注意してください。

多くの場合、インデックスとサイズが必要になると、それらに対して数学計算も実行され、size_tは、数学に使用されると「奇妙な」動作をします(たとえば、a+1 < ba < b-1は異なるものです)。

0
6502