web-dev-qa-db-ja.com

範囲ベースのforループはパフォーマンスに有益ですか?

C++イテレータとパフォーマンス**に関するスタックオーバーフローのさまざまな質問をここで読んだところ、for(auto& elem : container)がコンパイラによって最適なバージョンに「拡張」されるのかどうか疑問に思い始めましたか? (autoのようなもので、コンパイラはすぐに正しい型を推測するため、遅くなることはなく、時には速くなることもあります)。

**例えば、あなたが書くかどうかは重要ですか

for(iterator it = container.begin(), eit = container.end(); it != eit; ++it)

または

for(iterator it = container.begin(); it != container.end(); ++it)

無効化しないコンテナの場合?

39
TeaOverflow

標準はあなたの友達です、[stmt.ranged]/1を参照してください

フォームの範囲ベースのforステートメントの場合

for ( for-range-declaration : expression ) statement

range-initを括弧で囲まれた式と同等にする

( expression )

フォームの範囲ベースのforステートメント

for ( for-range-declaration : braced-init-list ) statement

range-initをbraced-init-listと同等にします。いずれの場合も、範囲ベースのforステートメントは

{
  auto && __range = range-init;
  for ( auto __begin = begin-expr,
             __end = end-expr;
        __begin != __end;
        ++__begin )
  {
    for-range-declaration = *__begin;
    statement
  }
}

したがって、はい、標準は可能な限り最良の形式が達成されることを保証します。

また、vectorなどの多くのコンテナでは、この反復中にコンテナを変更(挿入/消去)することは未定義の動作です。

33
Matthieu M.

Range-forは、エンドイテレータをキャッシュするため、可能な限り高速です[ 提供された引用 ]、プリインクリメントを使用し、イテレータを一度だけ逆参照します。

だからあなたが書く傾向があるなら:

for(iterator i = cont.begin(); i != cont.end(); i++) { /**/ }

次に、そうです、range-forはわずかに高速になる可能性があります。これは、適切な場合にそれを使用しない理由がないため、記述も簡単だからです。

N.B。私はそれが可能な限り高速であると述べましたが、それは可能な限り高速ではありません。手動ループを注意深く作成すると、まったく同じパフォーマンスを実現できます。

25
Motti

好奇心から、私は両方のアプローチのアセンブリコードを調べることにしました。

int foo1(const std::vector<int>& v) {
    int res = 0;
    for (auto x : v)
        res += x;
    return res;
}

int foo2(const std::vector<int>& v) {
    int res = 0;
    for (std::vector<int>::const_iterator it = v.begin(); it != v.end(); ++it)
      res += *it;
    return res;
}

また、アセンブリコード(-O3とgcc 4.6を使用)は両方のアプローチでまったく同じです(foo2のコードは完全に同じであるため省略されています)。

080486d4 <foo1(std::vector<int, std::allocator<int> > const&)>:
80486d4:       8b 44 24 04             mov    0x4(%esp),%eax
80486d8:       8b 10                   mov    (%eax),%edx
80486da:       8b 48 04                mov    0x4(%eax),%ecx
80486dd:       b8 00 00 00 00          mov    $0x0,%eax
80486e2:       39 ca                   cmp    %ecx,%edx
80486e4:       74 09                   je     80486ef <foo1(std::vector<int, std::allocator<int> > const&)+0x1b>
80486e6:       03 02                   add    (%edx),%eax
80486e8:       83 c2 04                add    $0x4,%edx
80486eb:       39 d1                   cmp    %edx,%ecx
80486ed:       75 f7                   jne    80486e6 <foo1(std::vector<int, std::allocator<int> > const&)+0x12>
80486ef:       f3 c3                   repz ret 

したがって、はい、両方のアプローチは同じです。

[〜#〜] update [〜#〜]vector<string>map<string, string>などの他のコンテナー(または要素タイプ)でも同じことが言えます。そのような場合、範囲ベースのループで参照を使用することが特に重要です。それ以外の場合は、一時ファイルが作成され、多くの追加コードが表示されます(前の例では、vectorにはint値のみが含まれていたため、必要ありませんでした)。

map<string, string>の場合、使用されるC++コードスニペットは次のとおりです。

int foo1(const std::map<std::string, std::string>& v) {
    int res = 0;
    for (const auto& x : v) {
        res += (x.first.size() + x.second.size());
    }
    return res;
}

int foo2(const std::map<std::string, std::string>& v) {
    int res = 0;
    for (auto it = v.begin(), end = v.end(); it != end; ++it) {
        res += (it->first.size() + it->second.size());
    }
    return res;
}

また、アセンブリコード(両方の場合)は次のとおりです。

8048d70:       56                      Push   %esi
8048d71:       53                      Push   %ebx
8048d72:       31 db                   xor    %ebx,%ebx
8048d74:       83 ec 14                sub    $0x14,%esp
8048d77:       8b 74 24 20             mov    0x20(%esp),%esi
8048d7b:       8b 46 0c                mov    0xc(%esi),%eax
8048d7e:       83 c6 04                add    $0x4,%esi
8048d81:       39 f0                   cmp    %esi,%eax
8048d83:       74 1b                   je     8048da0 
8048d85:       8d 76 00                lea    0x0(%esi),%esi
8048d88:       8b 50 10                mov    0x10(%eax),%edx
8048d8b:       03 5a f4                add    -0xc(%edx),%ebx
8048d8e:       8b 50 14                mov    0x14(%eax),%edx
8048d91:       03 5a f4                add    -0xc(%edx),%ebx
8048d94:       89 04 24                mov    %eax,(%esp)
8048d97:       e8 f4 fb ff ff          call   8048990 <std::_Rb_tree_increment(std::_Rb_tree_node_base const*)@plt>
8048d9c:       39 c6                   cmp    %eax,%esi
8048d9e:       75 e8                   jne    8048d88 
8048da0:       83 c4 14                add    $0x14,%esp
8048da3:       89 d8                   mov    %ebx,%eax
8048da5:       5b                      pop    %ebx
8048da6:       5e                      pop    %esi
8048da7:       c3                      ret    
22
betabandido

まれに、より高速になる可能性があります。イテレータに名前を付けることができないため、オプティマイザはループがイテレータを変更できないことをより簡単に証明できます。これは、例えばループ展開の最適化。

5
MSalters

いいえ。イテレータを使用した古いforループと同じです。結局のところ、範囲ベースのforはイテレータと内部的に連携します。コンパイラは、両方に対して同等のコードを生成するだけです。

4
Nawaz