時空の複雑さに関して、次のうちどれが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; ... */
まず、Way 2とWayは、事実上すべての標準ライブラリ実装で同じです。
それとは別に、投稿したオプションはほとんど同じです。唯一の注目すべき違いは、ウェイ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... */ });
_
しかし、これが通常のループよりも高速であるかどうかは、状況によって異なります。
vector
またはarray
の場合、どちらの形式にも違いはありません1、他のコンテナに入るのは良い習慣です。
1 もちろん、インデックスによるアクセスに.at()
の代わりに[]
を使用する限り。
各反復で終了境界を再計算することは、次の2つの理由で非効率的です。
あなたはワンライナーとしてそうすることができます:
for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }
(これは、一度に1つの変数を宣言することに対する一般的な禁止の例外です。)
For-eachループフォームは自動的に次のようになります。
したがって:
for (/*...*/ value : vec) { ... }
要素を値で取得することと、要素を参照で取得することの間には、明白ではないトレードオフがあります。
極端な場合、選択は明白なはずです。
int
、std::int64_t
、void*
、...)は値で取得する必要があります。std::string
、...)は、参照する必要があります。途中、または一般的なコードに直面したときは、参照から始めることをお勧めします。最後のサイクルを絞り出そうとするよりも、パフォーマンスの崖を避けた方がいいです。
したがって、一般的な形式は次のとおりです。
for (auto& element : vec) { ... }
そして、あなたが組み込みのものを扱っているなら:
for (int element : vec) { ... }
1 これは最適化の一般的な原則です。実際には、オプティマイザはローカル変数のすべての潜在的なエイリアス(またはその不在)を知っているため、ローカル変数はポインタ/参照よりも使いやすくなっています。
問題のコードをボトルネックとしてプロファイリングして検出しない限り、少なくともこのレベルのコードでは、効率(おそらく「有効性」の代わりに意味する)は最初の懸念事項ではありません。さらに重要なのは、コードの可読性と保守性です。したがって、最もよく読み取るループバリアントを選択する必要があります。これは通常、方法4です。
インデックスは、1より大きいステップがある場合に役立ちます(ただし、必要な場合は...):
for(size_t i = 0; i < v.size(); i += 2) { ... }
+= 2
それ自体はイテレータでも有効です。終了位置を超えてインクリメントするため、ベクトルのサイズが奇数の場合、ループの終了時に未定義の動作が発生する危険があります。 (一般的には、nをインクリメントすると、サイズがnの正確な倍数でない場合、UBが取得されます。これをキャッチするには追加のコードが必要ですが、インデックスバリアントは必要ありません...
怠惰な答え:複雑さは同等です。
さまざまなソリューションに含まれる定数要素は、実装の詳細です。数値が必要な場合は、特定のターゲットシステムでさまざまなソリューションのベンチマークを行うのが最善の方法です。
v.size()
rspを保存すると役立ちます。 v.end()
、ただしこれらは通常インライン化されるため、そのような最適化は必要ない場合があります。または 自動的に実行されます 。
(v.size()
をメモせずに)インデックスを作成することが、(Push_back()
を使用して)要素を追加する可能性のあるループ本体を正しく処理する唯一の方法であることに注意してください。ただし、ほとんどのユースケースでは、この追加の柔軟性は必要ありません。
メソッド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最適化が妨げられます。
ここで証明を見ることができます:
4/5/6だけがvmulps SIMD命令を使用していることに気づくでしょう。1/ 2/3は非SIMD vmulss命令しか使用していません。
注:godboltリンクでVC++を使用していますが、これは問題を適切に示しているためです。同じ問題がgcc/clangでも発生しますが、godboltでそれをデモンストレーションするのは容易ではありません。通常、これが発生していることを確認するには、DSOを分解する必要があります。
それはあなたが「効果的」で何を意味するかに大きく依存します。
他の回答では効率について言及していますが、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であると考え、「誰がこのゴミを書いたのか? "...
完全を期すために、ループがベクターのサイズを変更する可能性があることを述べておきたいと思います。
_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を使用することを好みますが、それは別の話です。
リストしたすべての方法は、同じ時間の複雑さと同じ空間の複雑さを持っています(そこに驚きはありません)。
for(auto& value : v)
構文を使用すると、他のメソッドでは、コンパイラーがテストを実行するたびにメモリからv.size()
およびv.end()
を再ロードする可能性があるため、わずかに効率的ですfor(auto& value : v)
の場合、これは発生しません(begin()
およびend()
イテレータを1回だけロードします)。
ここで、各メソッドによって生成されたアセンブリの比較を観察できます。 https://godbolt.org/z/LnJF6p
少しおかしいことに、コンパイラーはmethod3
をjmp
命令としてmethod2
に実装します。
コンテナの最後が一度だけ評価されるため、理論的にはより高速な最後の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 < b
とa < b-1
は異なるものです)。