【问题标题】:Is the ranged based for loop beneficial to performance?基于范围的 for 循环是否对性能有益?
【发布时间】:2012-06-05 00:16:03
【问题描述】:

阅读 StackOverflow 上有关 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)

对于非失效容器?

【问题讨论】:

  • 使用 auto 代替 typedefs 对性能有何帮助?
  • @JonathanWakely 对不起,提到 typedefs 是我的错误。我找不到合适的参考,但我提到的是 Stephen T. Lavavej 在 49:30+ 在 channel9.msdn.com/Events/GoingNative/GoingNative-2012/STL11-Magic-Secrets 中提到的内容

标签: c++ performance for-loop foreach c++11


【解决方案1】:

标准是您的朋友,请参阅 [stmt.ranged]/1

对于形式的基于范围的for语句

for ( for-range-declaration : expression ) statement

令 range-init 等价于括号内的表达式

( expression )

对于一个基于范围的for语句的形式

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

让 range-init 等价于花括号初始化列表。在每种情况下,基于范围的for 语句都等效于

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

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

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

【讨论】:

  • @Evgeni:在循环容器时修改容器通常是有问题的(在许多其他语言中也是如此)。通常有一些方法可以正确地做到这一点,但它们通常比基于范围的简单循环更困难。这里也是如此:您必须手动迭代并使用例如来自 erase 的结果更新迭代器
  • 您如何断定它可以保证最佳性能?我不清楚这个结论是如何得出的。
  • @thc:我不是说最好的性能,而是最好的形式。具体来说,请注意(1)range-init 只评估一次,(2)begin-exprend-expr 只评估一次,(3)使用预增量。从算法上讲,这是复杂度最低的形式。它是否优化到最佳性能是可能的,但不能保证。优化器是善变的。
【解决方案2】:

Range-for 尽可能快,因为它缓存了结束迭代器[citation provided],使用预增量并且只取消引用迭代器一次。

所以如果你倾向于写作:

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

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

注意我说它是尽可能快的,但它并不是比可能的更快。如果您仔细编写手动循环,您可以获得完全相同的性能。

【讨论】:

    【解决方案3】:

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

    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 
    

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

    更新:同样的观察结果适用于其他容器(或元素类型),例如 vector&lt;string&gt;map&lt;string, string&gt;。在这些情况下,在基于范围的循环中使用引用尤为重要。否则会创建一个临时代码并出现大量额外代码(在前面的示例中不需要它,因为 vector 只包含 int 值)。

    对于map&lt;string, string&gt;的情况,使用的C++代码sn-p是:

    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    
    

    【讨论】:

    • 编译器在这种情况下优化了v.end(),它可能并不总是能够这样做(对于其他容器)。
    • 就像@Motti 所说,这不是证据。
    • OP 还包括缓存v.end() 的选项,并且汇编代码看起来仍然相同。如果您对此更满意...
    • @Evgeni:不,你错了。这取决于end 的复杂性、循环体的透明度(如果您将容器的引用传递给不透明的函数,那么所有的赌注都没有)以及优化器是否足以推断出它。尽管大多数书籍都这样做,但最好无条件地缓存 end() 调用 for-each 等价物。它还记录了end() 对于未来的读者来说不会膨胀。
    • 请注意,使用 std::for_each 也会产生相同的编译代码。
    【解决方案4】:

    没有。它与带有迭代器的旧 for 循环相同。毕竟,基于范围的for 在内部与迭代器一起工作。编译器只会为两者生成等效的代码。

    【讨论】:

    • 我知道它使用迭代器,但想知道编译器是否会使用迭代器生成最好的循环版本...
    【解决方案5】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-10-31
      • 2011-11-03
      • 1970-01-01
      • 1970-01-01
      • 2014-12-06
      • 2014-01-12
      相关资源
      最近更新 更多