【问题标题】:C++ iterators & loop optimizationC++ 迭代器和循环优化
【发布时间】:2010-10-22 05:05:42
【问题描述】:

我看到很多类似这样的 c++ 代码:

for( const_iterator it = list.begin(),
     const_iterator ite = list.end();
     it != ite; ++it)

相对于更简洁的版本:

for( const_iterator it = list.begin();
     it != list.end(); ++it)

这两种约定的速度会有什么不同吗?因为 list.end() 只被调用一次,所以第一个会稍微快一些。但由于迭代器是 const,编译器似乎会将这个测试拉出循环,为两者生成等效的程序集。

【问题讨论】:

  • 'ite' 的声明将是一个语法错误,因此您的第一个版本变为“for (const_iterator i = list.begin(), e = list.end(); i != e; + +i)"。这只是比第二种形式多几个字符,所以我只是默认使用它。
  • 现在在 C++11 中还有for(auto it : list),它本质上是第二个。但是好多了。
  • @Cramer 基于范围的 for 循环遍历元素,而不是迭代器位置,因此等价于 for ( const auto& element : list )
  • @boycy,如果您想成为技术人员,两者都不相同,因为迭代器都不是。尝试访问element->whatever
  • @Cramer,范围循环相当于第一种形式,而不是第二种形式(en.cppreference.com/w/cpp/language/range-for

标签: c++ optimization compiler-construction coding-style iterator


【解决方案1】:
  1. 在压力条件下对其进行采样,看看您是否经常在**此代码中***。
    如果不是,没关系。

  2. 如果是,请查看反汇编,或单步执行。
    这样您就可以判断哪个更快。

您必须小心这些迭代器。
它们可能会被优化为漂亮的机器代码,但通常情况下它们不会,并且会浪费时间。

**(其中“in”表示实际在其中,或从中调用。)

***(其中“经常”表示相当大比例的时间。)

添加:不要只查看代码每秒执行多少次。它可能每秒 1,000 次,但仍然使用不到 1% 的时间。

也不要计算需要多长时间。这可能需要一毫秒,但仍然使用不到 1% 的时间。

您可以将两者相乘以获得更好的主意,但这只有在它们不太偏斜的情况下才有效。

Sampling the call stack 会告诉您它是否使用了足够高的时间百分比。

【讨论】:

    【解决方案2】:

    编译器可能能够将第二个优化为第一个,但假设两者是等价的,即 end() 实际上是恒定的。一个稍微有问题的问题是编译器可能由于可能的别名而无法推断出结束迭代器是恒定的。但是,假设对 end() 的调用是内联的,区别只是内存负载。

    请注意,这假定优化器已启用。如果优化器没有启用,就像调试构建中经常做的那样,那么第二个公式将涉及 N-1 多个函数调用。在当前版本的 Visual C++ 中,由于函数 prolog/epilog 检查和更重的调试迭代器,调试构建也会产生额外的命中。因此,在 STL 繁重的代码中,默认为第一种情况可以防止代码在调试构建中异常缓慢。

    正如其他人所指出的那样,在循环中插入和删除是可能的,但是对于这种循环样式,我发现这不太可能。一方面,基于节点的容器——list、set、map——不会在任一操作中使 end() 无效。其次,迭代器增量必须经常在循环中移动以避免失效问题:

    // 假设列表 -- 不能为向量缓存 end() iterator it(c.begin()), end(c.end()); 而(它!=结束){ if (should_remove(*it)) 它= c.erase(它); 别的 ++它; }

    因此,我认为一个循环声称调用 end() 出于 mutate-during-loop 的原因,并且在循环头中仍然有 ++it 是可疑的。

    【讨论】:

      【解决方案3】:

      我只想记录一下,C++ 标准要求在 any 容器类型上调用 begin()end()(可能是 vectorlistmap等)必须只花费恒定的时间。实际上,如果您在打开优化的情况下进行编译,这些调用几乎肯定会被内联到单个指针比较中。

      请注意,此保证不一定适用于其他供应商提供的“容器”,这些“容器”实际上不符合标准第 23 章中规定的容器的形式要求(例如单链表slist )。

      【讨论】:

      • ++ 从我的所见所闻,迭代器有时/经常不会内联到无调用代码,即使它们花费恒定的时间,时间也可能是彻头彻尾的小猪。当然,这可能没问题,直到你陷入压力境地,然后这可能是你最大的代价。道德:注意可能性。
      • @Mike:Sing Yip 提出了一个很好的观点——内联实际上只能发生在同一个翻译单元中包含的函数(例如,通过头文件)。我很想看到一个代码 sn-p,其中 STL 容器的 end() (可重现地)没有被最近的(
      • FWIW 可以使用 gcc 的 -flto flag 恢复跨翻译单元的函数内联损失,以启用“链接时间优化”。 Clang 有一个similar feature
      【解决方案4】:

      虽然这两个版本不一样。在第二个版本中,它每次都将迭代器与list.end() 进行比较,list.end() 的评估结果可能会在循环过程中发生变化。现在当然不能通过 const_iterator it 修改list;但没有什么能阻止循环内的代码直接调用list 上的方法并对其进行变异,这可能(取决于list 是哪种数据结构)改变结束迭代器。因此,在某些情况下,预先存储结束迭代器可能是不正确的,因为当您访问它时,它可能不再是正确的结束迭代器。

      【讨论】:

      • +1。关于列表更改可能性的好点,只有第二个代码 sn-p 才能正确处理。
      • 如果 list 是 std::vector,例如,在循环内更改它会使所有迭代器无效,从而使两个循环都不正确。
      • 这才是真正的答案。
      【解决方案5】:

      考虑这个例子:

      for (const_iterator it = list.begin(); it != list.end(); ++list)
      {
          if (moonFull())
              it = insert_stuff(list);
          else
              it = erase_stuff(list);
      }
      

      在这种情况下,您需要在循环中调用 list.end(),而编译器不会对其进行优化。

      编译器可以证明 end() 总是返回相同值的其他情况,可以进行优化。

      如果我们谈论的是 STL 容器,那么当编程逻辑不需要多个 end() 调用时,我认为任何好的编译器都可以优化多个 end() 调用。但是,如果您有一个自定义容器并且 end() 的实现不在同一个翻译单元中,那么优化将不得不在链接时进行。我对链接时间优化知之甚少,但我敢打赌大多数链接器不会做这样的优化。

      【讨论】:

      • 但是如果你使用迭代器来循环列表,你不应该也使用迭代器来修改列表吗?否则,当数据和迭代器不同步时,您可能会遇到奇怪的并发问题。也许这在 Java 中是一个更大的问题,因为迭代器有更多的内容。
      • 是的,你是对的。编写 insert__stuff(it, list) 会更有意义......但我试图理解的一点是 list 可以在循环内更改并且必须为每个循环调用 list.end() 。跨度>
      • +1。你关于内联的观点只有在 end() 的定义出现在同一个翻译单元中时才会发生,这对我来说非常有意义。我想知道当其他人抱怨编译器缺少“明显”的内联机会时,这是否是他们所经历的......?
      • 这是 IMO 的正确答案。您甚至可以通过调用任何非内联函数来重现对 end() 的多次调用:` for (auto it = list.cbegin(); it != list.cend(); ++list) { (... ) 富(); }`
      【解决方案6】:

      啊,人们似乎在猜测。在调试器中打开你的代码,你会看到对 begin()、end() 等的调用都被优化掉了。无需使用版本 1。使用 Visual C++ 编译器 fullopt 测试。

      【讨论】:

      • 这将取决于编译器、容器和优化设置。最好消除所有疑虑。
      • 这取决于有问题的循环。我在过去发现了几种情况,MSVC++ 没有将第二种情况优化为第一种情况,即使看起来很明显应该这样做。
      • 但是从一个数据点得出结论实际上并不比猜测更好。
      【解决方案7】:

      我总是喜欢第一个。尽管使用内联函数、编译器优化和相对较小的容器大小(在我的情况下通常最多 20-25 个项目),但它在性能方面确实没有任何大的差异。

      const_iterator it = list.begin();
      const_iterator endIt = list.end();
      
      for(; it != endIt ; ++it)
      {//do something
      }
      

      但最近我尽可能多地使用std::for_each。它优化的循环有助于使代码看起来比其他两个更具可读性。

      std::for_each(list.begin(), list.end(), Functor());
      

      只有当std::for_each 不能使用时,我才会使用循环。 (例如:std::for_each 不允许您中断循环,除非抛出异常)。

      【讨论】:

      • 我不知道这个功能。与内置的“for”相比,函数调用似乎总是有很大的开销,尽管它非常可读。即使使用宏实现,它也不会比 for 循环快。这让我希望我可以在这个项目中使用 python(不幸的是我的雇主要求使用 c++)。
      • @Quantum7:当然它不能比循环(因为在内部它被转换为循环),但几乎可以肯定不会慢。跨度>
      • @Quantum7 It seems like a function call will always have significant overhead 这显然是错误的;内联是存在的。
      【解决方案8】:

      您可以使第一个版本更简洁,并充分利用两者:

      for( const_iterator it = list.begin(), ite = list.end();
           it != ite; ++it)
      

      附:迭代器不是 const,它们是 const 引用的迭代器。有很大的不同。

      【讨论】:

        【解决方案9】:

        理论上,编译器可以将第二个版本优化为第一个版本(显然,假设容器在循环期间没有改变)。

        在实践中,我在分析时间关键代码时发现了几个类似的情况,其中我的编译器完全无法将不变计算提升到循环条件之外。因此,虽然在大多数情况下稍微更简洁的版本都可以,但在我真正关心性能的情况下,我不依赖编译器对其进行明智的处理。

        【讨论】:

        • 我认为问题不在于编译器是否足够聪明,可以检测到 end() 是不变的并将其提升到循环之外(这需要一个相对聪明的编译器)——而是是否调用end() 可以内联(不需要如此智能的编译器),因为 end() 中的代码通常非常简短,例如std::vector 或 std::list 的单指针比较。
        【解决方案10】:

        我会选择最简洁易读的选项。不要试图猜测编译器及其可能执行的优化。请记住,您的绝大多数代码对整体性能绝对没有影响,因此只有在代码的性能关键部分中,您才应该花时间对其进行分析并选择适当有效的源代码表示。

        具体参考您的示例,第一个版本复制 end() 迭代器,调用为迭代器对象的复制构造函数运行的任何代码。 STL 容器通常包含内联的end() 函数,因此即使您不想帮助编译器,编译器也有很多机会优化第二个版本。哪一个最好?测量它们。

        【讨论】:

          【解决方案11】:

          第一个可能几乎总是更快,但如果您认为这会有所不同,请始终先配置文件,看看哪个更快以及快多少。

          编译器可能会在这两种情况下内联对end() 的调用,尽管如果end() 足够复杂,它可能会选择不内联它。但是,关键的优化是编译器是否可以执行loop-invariant code motion。我假设在大多数情况下,编译器无法确定 end() 的值在循环迭代期间不会改变,在这种情况下,它别无选择,只能在每次迭代后调用 end()

          【讨论】:

          • 我同意。最好先编写易于阅读的代码。然后,如果有任何性能问题 - 分析代码,确保循环条件是瓶颈,然后才重写为更快但可读性稍差的版本。
          • 为这两种方法计时是个好主意。您知道第一个会更快,因为对 end() 的每次调用几乎肯定会被内联到单个指针比较中。此外,C++ 标准保证在任何容器上调用 end() 都是一个常量时间操作,因此它永远不会“足够复杂”。
          猜你喜欢
          • 2013-05-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-11
          相关资源
          最近更新 更多