C ++迭代器&循环优化

时间:2009-04-28 02:16:17

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

我看到很多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,似乎编译器会将此测试从循环中拉出来,为两者生成等效的汇编。

11 个答案:

答案 0 :(得分:43)

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

答案 1 :(得分:29)

我只是提到记录,C ++标准要求在任何容器类型上调用begin()end()(是vector,{ {1}},list等。必须只占用一段时间。实际上,如果在启用优化的情况下进行编译,这些调用几乎肯定会被内联到单个指针比较中。

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

答案 2 :(得分:11)

第一个可能几乎总是更快,但是如果你认为这会有所不同,那么总是配置文件,看看哪个更快,多少。

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

答案 3 :(得分:8)

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

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

答案 4 :(得分:6)

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

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

P.S。迭代器不是const,它们是const引用的迭代器。这有很大的不同。

答案 5 :(得分:6)

考虑这个例子:

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()的实现不在同一个转换单元中,那么优化必须在链接时进行。我对链接时间优化知之甚少,但我敢打赌,大多数链接器都不会进行这样的优化。

答案 6 :(得分:4)

啊,人们似乎正在猜测。在调试器和放大器中打开代码。你会看到对begin(),end()等的调用都被优化掉了。没有必要使用版本1.使用Visual C ++编译器fullopt进行测试。

答案 7 :(得分:4)

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

请注意,这假定启用了优化程序。如果未启用优化器(通常在调试版本中执行),则第二个公式将涉及N-1个更多函数调用。在当前版本的Visual C ++中,由于函数prolog / epilog检查和较重的调试迭代器,调试版本也会产生额外的命中。因此,在STL繁重的代码中,默认为第一种情况可以防止代码在调试版本中不成比例地慢。

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

   // assuming list -- cannot cache end() for vector
   iterator it(c.begin()), end(c.end());
   while(it != end) {
       if (should_remove(*it))
           it = c.erase(it);
       else
           ++it;
   }

因此,我认为一个循环声称调用end()是因为循环中出现异常的原因,并且在循环头中仍然有++它是可疑的。

答案 8 :(得分:2)

  1. 在压力条件下对它进行取样,看看你是否经常使用这个代码***。如果不是,那就无所谓了。

  2. 如果你是,请查看反汇编或单步执行。
    这就是你怎么知道哪个更快。

  3. 你必须小心这些迭代器。
    它们可能会被优化成漂亮的机器代码,但通常它们不会,并且变成时间生长。

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

    ***(“经常”意味着很大一部分时间。)

    ADDED:不要只看每秒执行代码的次数。它可能是每秒1000次,并且仍然使用不到1%的时间。

    不要花费多长时间。它可能需要一毫秒,并且仍然使用不到1%的时间。

    你可以将这两者相乘,以获得更好的想法,但这只有在它们没有太倾斜的情况下才有效。

    Sampling the call stack会告诉您它是否使用了足够长的时间来解决问题。

答案 9 :(得分:1)

我总是喜欢第一个。虽然内联函数,编译器优化和相对较小的容器大小(在我的情况下通常最多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不允许你打破循环,除非抛出异常)。

答案 10 :(得分:0)

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

在实践中,我在分析时间关键代码时发现了几个类似的情况,其中我的编译器完全无法在循环条件下提升不变量计算。因此,虽然在大多数情况下稍微更简洁的版本都很好,但我并不依赖于编译器在我真正关心性能的情况下使用它做出明智的事情。