C ++迭代器和循环优化

我看到很多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的,所以编译器似乎会将这个testing从循环中拉出来,为两者产生相同的程序集。

我只是提到logging,C ++标准要求在任何容器types(无论是vectorlistmap等)上调用begin()end() 都只需要一个固定的时间 。 在实践中,如果在优化开启的情况下进行编译,这些调用几乎肯定会被内联到单个指针比较。

请注意,这一保证不一定适用于其他供应商提供的“容器”,这些“容器”实际上并不遵守标准第23章中列出的容器的正式要求(例如,单链表清单)。

这两个版本不尽相同。 在第二个版本中,它每次比较迭代器和list.end() ,以及list.end()在循环过程中计算的结果。 现在当然,你不能通过const_iterator修改list , 但是没有任何东西可以阻止循环内的代码直接调用list方法并对其进行变异,这可能(取决于哪种数据结构list )更改结束迭代器。 在某些情况下,事先存储结束迭代器可能是不正确的,因为在达到结束迭代器时,这可能不再是正确的结束迭代器。

第一个可能几乎总是会更快,但如果你认为这会有所作为,总是首先查看哪个更快,更多。

在这两种情况下,编译器都可能内联调用end() ,但如果end()足够复杂,可以select不内联。 然而,关键的优化是编译器是否可以执行循环不变的代码运动 。 我认为在大多数情况下,编译器不能确定end()的值在循环的迭代期间不会改变,在这种情况下,除了在每次迭代之后调用end()别无select。

我会select最简洁和可读的选项。 不要试图猜测编译器和它可能执行的优化。 请记住,绝大多数代码对整体性能没有任何影响,所以只有在代码性能严重的部分中,您才能花时间来分析代码,并select一个合适的代码。

具体参考你的例子,第一个版本制作了end()迭代器的副本,调用迭代器对象的拷贝构造函数的任何代码运行。 STL容器通常包含内联end()函数,所以编译器有很多机会来优化第二个版本,即使你没有试图帮助它。 哪一个最好? 衡量他们。

你可以使第一个版本更简洁,并获得最好的两个:

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

PS迭代器不是const的,它们是const引用的迭代器。 有一个很大的区别。

考虑这个例子:

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

在这种情况下,你需要在循环中调用list.end(),编译器不会优化它。

其他情况下,编译器可以certificateend()始终返回相同的值,可以进行优化。

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

啊,人们似乎在猜测。 在debugging器中打开你的代码,你会看到开始(),结束()等等的一切都被优化了。 不需要使用版本1.使用Visual C ++编译器fullopt进行testing。

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

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

正如其他人所指出的那样,循环中的插入和删除是可能的,但是对于这种循环风格我觉得不太可能。 首先,基于节点的容器 – 列表,设置,映射 – 不会使任何操作的end()失效。 其次,为了避免失效问题,迭代器的频繁增量必须移入循环中:

    //假定列表 - 不能caching结束()的向量
   迭代器它(c.begin()),结束(c.end());
    while(it!= end){
        if(should_remove(* it))
            it = c.erase(it);
       其他
            ++它;
    } 

因此,我考虑一个循环,声称在循环期间调用end()来实现mutate,并且在循环头文件中仍然有++值得怀疑。

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

  1. 在压力条件下对其进行采样,看看你是否经常**这个代码***。
    如果不是,那没关系。

  2. 如果你是,看看反汇编,或单步它。
    这就是你可以判断哪个更快。

你必须小心这些迭代器。
他们可能会被优化成漂亮的机器码,但他们往往不够,并成为时间消耗。

**(其中“in”的意思是实际上或者是从中被调用)

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

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

不要花费多长时间。 这可能需要一毫秒的时间,而且还不到1%的时间。

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

调用堆栈的采样会告诉你是否使用足够高的时间百分比。

理论上,编译器可以将第二个版本优化到第一个版本(假设容器在循环中不会改变)。

实际上,在分析时间关键的代码时,我发现了几个类似的情况,其中我的编译器完全没有提出超出循环条件的不变计算。 所以虽然稍微简洁一些的版本在大多数情况下都可以,但是我并不依赖于编译器对于性能真正关注的情况。