web-dev-qa-db-ja.com

C ++のループでのvector :: size()のパフォーマンスの問題

次のコードでは:

_std::vector<int> var;
for (int i = 0; i < var.size(); i++);
_

size()メンバー関数は、ループの反復ごとに呼び出されますか、それとも1回だけ呼び出されますか?

35
Ismail Marmoush

理論的には、forループなので、毎回呼び出されます。

for(initialization; condition; increment)
    body;

次のようなものに拡張されます

{
    initialization;
    while(condition)
    {
        body;
        increment;
    }
}

(初期化はすでに内部スコープにあるため、中括弧に注意してください)

実際には、コンパイラが条件の一部がループのすべての期間を通じて不変であることを理解し、副作用がない、それは十分に賢くなりますそれを移動します。これは、引数が記述されていないループでstrlenなど(コンパイラがよく知っている)を使用して日常的に実行されます。

ただし、この最後の条件を証明するのは必ずしも簡単ではないことに注意する必要があります。一般に、コンテナが関数に対してローカルであり、外部関数に渡されない場合は簡単です。コンテナーがローカルではなく(たとえば、参照によって渡された場合-constであっても)、ループ本体に他の関数への呼び出しが含まれている場合、コンパイラーは、そのような関数がコンテナーを変更する可能性があると想定しなければならないことがよくあります。長さの計算の。

条件の一部を評価するのに「費用がかかる」ことがわかっている場合は、手動で最適化を行う価値があります(通常、このような条件は、ほぼ確実にインライン化されるポインター減算に要約されるため、通常はそうではありません)。


編集: 他の人が言ったように、一般にコンテナではイテレータを使用する方が良いですが、operator[]を介した要素へのランダムアクセスはO(1)であることが保証されているため、vectorsの場合はそれほど重要ではありません。実際には、ベクトルの場合、通常はポインターの合計(ベクトルベース+インデックス)と逆参照対ポインターインクリメント(前の要素+ 1)とイテレーターの逆参照です。ターゲットアドレスはまだ同じなので、キャッシュの局所性の観点からイテレータから何かを得ることができるとは思いません(たとえそうだとしても、タイトなループで大きな配列を歩いていない場合は、そのようなことに気付かないはずです一種の改善)。

リストやその他のコンテナの場合、代わりに、ランダムアクセスの代わりにイテレータを使用することが重要になる可能性があります本当にランダムアクセスを使用すると、リストごとに歩くことを意味する可能性がありますが、イテレータをインクリメントすることは単なるポインタの逆参照です。

46
Matteo Italia

毎回「呼び出される」のですが、実際にはインラインメソッド呼び出しである可能性が高いため、引用符で囲んでいます。そのため、パフォーマンスについて心配する必要はありません。

代わりにvector<int>::iteratorを使用してみませんか?

5

size()メンバー関数は毎回呼び出されますが、インライン化されない非常に悪い実装であり、固定データムまたは減算への単純なアクセスではない奇妙な実装です。 2つのポインタの。
とにかく、アプリケーションのプロファイルを作成し、これがボトルネックであることがわかるまで、このような些細なことで心配する必要はありません。

ただし、should注意する必要があるのは次のとおりです。

  1. ベクトルのインデックスの正しいタイプはstd::vector<T>::size_typeです。
  2. i++might++iよりも遅いタイプ(一部のイテレータなど)があります。

したがって、ループは次のようになります。

for(vector<int>::size_type i=0; i<var.size(); ++i)
  ...
5
sbi

Size()は毎回異なる値を返す可能性があるため、毎回呼び出す必要があります。

したがって、それが単にあるに違いない大きな選択はありません。

2
Vinzenz

I thinkコンパイラが、変数varが「ループ本体」内で変更されていないと決定的に推測できる場合

_for(int i=0; i< var.size();i++) { 
    // loop body
}
_

次に、上記は同等のものに置き換えることができます

_const size_t var_size = var.size();
for( int i = 0; i < var_size; i++ ) { 
    // loop body
}
_

しかし、私は絶対に確信が持てないので、コメントは大歓迎です:)

また、

  • ほとんどの場合、size()メンバー関数はインライン化されているため、この問題は心配する必要はありません。

  • この懸念は、イテレータベースのループに常に使用されるend()、つまりit != container.end()にも同様に当てはまります。

  • iのタイプには_size_t_または_vector<int>::size_type_の使用を検討してください[以下のSteveJessopのコメントを参照してください。]

1
Arun

他の人が言ったように

  • セマンティクスは、毎回呼び出されたかのようにする必要があります
  • おそらくインライン化されており、おそらく単純な関数です

その上に

  • 十分に賢いオプティマイザーは、それが副作用のないループ不変であると推測し、それを完全に排除することができるかもしれません(これはコードがインライン化されている場合は簡単ですが、そうでない場合でも可能かもしれませんifコンパイラはグローバル最適化を行います)

しかし、それはこの方法で行うことができます(このループが実際にベクトルのサイズを変更せずに読み取り/書き込みのみを意図している場合):

for(vector<int>::size_type i=0, size = var.size(); i < size; ++i) 
{
//do something
}

上記のループでは、サイズがインライン化されているかどうかに関係なく、サイズを1回呼び出すだけです。

あなたの質問の問題は、それが意味をなさないということです。 C++コンパイラは、一部のソースコードをバイナリプログラムに変換します。要件は、結果のプログラムがC++標準の規則に従ってコードの観察可能な効果を保持する必要があることです。このコード:

_for (int i = 0; i < var.size(); i++); 
_

単に観察可能な効果はありません。さらに、周囲のコードとはまったく相互作用せず、コンパイラーは完全に最適化する可能性があります。つまり、対応するアセンブリを生成しません。

質問を意味のあるものにするには、ループ内で何が起こるかを指定する必要があります。の問題

_for (int i = 0; i < var.size(); i++) { ... }
_

答えは_..._が実際に何であるかに大きく依存するということです。 @MatteoItaliaは非常に良い答えを提供したと思います。私が行ったいくつかの実験の説明を追加するだけです。次のコードについて考えてみます。

_int g(std::vector<int>&, size_t);

int f(std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0; i < v.size(); i++)
      res += g(v, i);
   return res;
}
_

まず、var.size()の呼び出しがほぼ100%確実に有効な最適化でインライン化され、このインライン化は通常2つのポインターの減算に変換されますが、これはループにいくらかのオーバーヘッドをもたらします。コンパイラがベクトルサイズが保持されていることを証明できない場合(これは、一般に、非常に困難であるか、この場合のように実行不可能です)、不要なloadおよびsub(および、場合によってはshift)命令。 GCC 9.2、_-O3_、およびx64で生成されたループのアセンブリは次のとおりです。

_.L3:
    mov     rsi, rbx
    mov     rdi, rbp
    add     rbx, 1
    call    g(std::vector<int, std::allocator<int> >&, unsigned long)
    add     r12d, eax
    mov     rax, QWORD PTR [rbp+8] // loads a pointer
    sub     rax, QWORD PTR [rbp+0] // subtracts another poniter
    sar     rax, 2                 // result * sizeof(int) => size()
    cmp     rbx, rax
    jb      .L3
_

コードを次のように書き直すと、次のようになります。

_int g(std::vector<int>&, size_t);

int f(std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0, e = v.size(); i < e; i++)
      res += g(v, i);
   return res;
}
_

次に、生成されたアセンブリはより単純になります(したがって、より高速になります)。

_.L3:
    mov     rsi, rbx
    mov     rdi, r13
    add     rbx, 1
    call    g(std::vector<int, std::allocator<int> >&, unsigned long)
    add     r12d, eax
    cmp     rbx, rbp
    jne     .L3
_

ベクトルのサイズの値は、単にレジスタ(rbp)に保持されます。

ベクトルがconstとしてマークされている別のバージョンも試しました。

_int g(const std::vector<int>&, size_t);

int f(const std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0; i < v.size(); i++)
      res += g(v, i);
   return res;
}
_

驚いたことに、ここでv.size()を変更できない場合でも、生成されたアセンブリは最初のケースと同じでした(movsub、およびsarが追加されています)指示)。

ライブデモは ここ です。

さらに、ループを次のように変更したとき:

_for (size_t i = 0; i < v.size(); i++)
   res += v[i];
_

その場合、アセンブリレベルのループ内でv.size()(ポインタの減算)の評価はありませんでした。 GCCはここで、ループの本体がサイズを変更しないことを「見る」ことができました。

0
Daniel Langr

900k回の反復でテスト済み。事前に計算されたサイズの場合は43秒、size()呼び出しの使用の場合は42秒かかります。

ベクトルサイズがループ内で変化しないことが保証されている場合は、事前に計算されたサイズを使用することをお勧めします。そうでない場合は、選択の余地がなく、size()を使用する必要があります。

#include <iostream>
#include <vector>

using namespace std;

int main() {
vector<int> v;

for (int i = 0; i < 30000; i++)
        v.Push_back(i);

const size_t v_size = v.size();
for(int i = 0; i < v_size; i++)
        for(int j = 0; j < v_size; j++)
                cout << "";

//for(int i = 0; i < v.size(); i++)
//      for(int j = 0; j < v.size(); j++)
//              cout << "";
}
0
Sree

他の人が言ったように、コンパイラは書かれた実際のコードをどうするかを決定しなければなりません。重要なのは、毎回呼び出されることです。ただし、パフォーマンスを向上させたい場合は、いくつかの考慮事項を考慮してコードを作成することをお勧めします。あなたのケースはそのうちの1つですが、これら2つのコードの違いのように、他にもあります。

for (int i = 0 ; i < n ; ++i)
{
   for ( int j = 0 ; j < n ; ++j)
       printf("%d ", arr[i][j]);
   printf("\n");
}
for (int j = 0 ; j < n ; ++j)
{
   for ( int i = 0 ; i < n ; ++i)
       printf("%d ", arr[i][j]);
   printf("\n");
}

違いは、最初のものは参照ごとにramページをあまり変更しませんが、もう1つはキャッシュやTLBなどを使い果たしてしまうことです。

またインラインはそれほど役に立ちません!呼び出し元の関数の順序はn(ベクトルのサイズ)回のままであるためです。いくつかの場所で役立ちますが、最良のことはコードを書き直すことです。

だが!コンパイラにコードの最適化を行わせたい場合は、次のように揮発性を絶対に入れないでください。

for(volatile int i = 0 ; i < 100; ++i)

コンパイラが最適化するのを防ぎます。パフォーマンスに関する別のヒントが必要な場合は、volatileではなくregisterを使用してください。

for(register int i = 0 ; i < 100; ++i)

コンパイラは、iをCPUレジスタからRAMに移動しないようにします。それができるかどうかは保証されていませんが、最善を尽くします;)

0
Amir Zadeh