基于for循环的范围是否有益于性能?

Tea*_*low 35 c++ performance foreach for-loop c++11

在Stack Overflow上阅读关于C++迭代器和性能的各种问题**,我开始想知道for(auto& elem : container)编译器是否"扩展"到最佳版本?(有点像auto,编译器会立即推断出正确的类型,因此永远不会更慢,有时更快).

**例如,如果你写,这是否重要

for(iterator it = container.begin(), eit = container.end(); it != eit; ++it)
Run Code Online (Sandbox Code Playgroud)

要么

for(iterator it = container.begin(); it != container.end(); ++it)
Run Code Online (Sandbox Code Playgroud)

对于无效的容器?

Mat*_* M. 32

标准是你的朋友,见[stmt.ranged]/1

对于基于范围的表单声明

for ( for-range-declaration : expression ) statement
Run Code Online (Sandbox Code Playgroud)

让range-init等同于括号括起来的表达式

( expression )
Run Code Online (Sandbox Code Playgroud)

以及基于范围的表格声明

for ( for-range-declaration : braced-init-list ) statement
Run Code Online (Sandbox Code Playgroud)

让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
  }
}
Run Code Online (Sandbox Code Playgroud)

所以,是的,标准保证实现最佳形式.

对于许多容器,例如vector,在此迭代期间修改(插入/擦除)它们是未定义的行为.

  • @thc:我不是说最好的表现,而是最好的形式。具体来说,请注意 (1) `range-init` 只计算一次,(2) `begin-expr` 和 `end-expr` 只计算一次,(3) 使用预增量。从算法上讲,这是最不复杂的形式。它是否优化到最佳性能是可能的,但不能保证。优化器是善变的。 (2认同)

Mot*_*tti 25

Range-for尽可能快,因为它缓存了结束迭代器[ 引用提供 ],使用预增量并且只对迭代器进行一次解引用.

所以,如果你倾向于写:

for(iterator i = cont.begin(); i != cont.end(); i++) { /**/ }
Run Code Online (Sandbox Code Playgroud)

那么,是的,range-for可能快一些,因为它也更容易编写,没有理由不使用它(在适当的时候).

NB我说它尽可能快,但速度并不.如果仔细编写手动循环,则可以获得完全相同的性能.


bet*_*ido 21

出于好奇,我决定查看两种方法的汇编代码:

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;
}
Run Code Online (Sandbox Code Playgroud)

并且汇编代码(使用-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 
Run Code Online (Sandbox Code Playgroud)

所以,是的,两种方法都是一样的.

更新:同样的观察适用于其他容器(或元素类型),如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;
}
Run Code Online (Sandbox Code Playgroud)

汇编代码(对于这两种情况)是:

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    
Run Code Online (Sandbox Code Playgroud)

  • 在这种情况下,编译器优化了`v.end()`,它可能并不总是能够这样做(对于其他容器). (11认同)
  • @Evgeni:不,你错了.它取决于`end`的复杂性,循环体的透明度(如果你将对容器的引用传递给不透明的函数,所有的赌注都是关闭的)以及优化器是否足以推断它.尽管大多数书都做了,但无条件地*更好地缓存`end()`调用for-equiv等价物.它还记录了`end()`不会为未来的读者而膨胀. (3认同)
  • 请注意,使用 std::for_each 也会产生相同的编译代码。 (2认同)

Naw*_*waz 5

不。它for与带有迭代器的旧循环相同。毕竟,基于范围的for在内部使用迭代器。编译器只是为两者生成等效的代码。


MSa*_*ers 5

在极少数情况下,它可能更快.由于您无法命名迭代器,因此优化器可以更轻松地证明您的循环无法修改迭代器.这会影响例如循环展开优化.