【问题标题】:Can end() be a costly operation for stl containersend() 能否成为 stl 容器的昂贵操作
【发布时间】:2012-04-03 20:04:48
【问题描述】:

https://doc-snapshots.qt.io/qtcreator-extending/coding-style.html 上,建议编写如下 for 循环:

Container::iterator end = large.end();
for (Container::iterator it = large.begin(); it != end; ++it) {
        //...;
}

而不是

for (Container::iterator it = large.begin(); it != large.end(); ++it) {
        //...;
}

由于我很少在任何代码中看到这种风格,我想知道 end() 的连续调用是否真的为 stl 容器上的大型循环增加了明显的运行时开销,或者编译器是否已经优化了这种情况。

编辑: 正如许多非常优秀的 cmets 所指出的那样:这个问题只有在循环内的代码确实不修改结束迭代器时才有效。否则,end 的重复调用当然是强制性的。

【问题讨论】:

  • 顺便说一句:我什至会选择for (Container::iterator it = large.begin(), end = large.end(); it != end; ++it) { ... },以便将变量end 的范围限制为for循环。
  • C# 和 Java 开发人员编写这种循环来让 JITer 对其进行优化(每次迭代少检查一次)。 C++ 似乎不是这样。
  • C++ 开发者只写for_each(begin(c), end(c), [](){}); 循环是为库编写者准备的:P
  • @MSalter:比方说 C+11 开发人员。 Lambda 与例如提升不适合胆小的人

标签: c++ performance stl containers


【解决方案1】:

取决于实现,但我不认为 end() 会带来那么大的性能延迟。

【讨论】:

    【解决方案2】:

    如果您打算在迭代时修改集合,则必须以第二种方式进行(结束可以更改) - 否则理论上第一种方式会快一点。不过,我怀疑它是否会引起注意。

    【讨论】:

      【解决方案3】:

      这与end 的成本无关,更多的是关于编译器能够看到end 不会因循环体中的副作用而改变(即它是循环不变量)。

      标准要求end 的复杂度保持不变。请参阅23.2.1 中 N3337 中的表 96。

      使用标准库算法很好地规避了整个困境。

      【讨论】:

        【解决方案4】:

        C++11 标准(第 23.2.1 节)要求 end 具有 O(1) 复杂度,因此符合标准的实现对于两个版本将具有相同的性能特征。

        也就是说,除非编译器可以证明end 的返回值永远不会改变,否则将end 拉出循环可能会更快一些(如 Steve Jessop cmets ,有很多变量可以影响这是否正确)。

        尽管如此,即使在某个特定情况下绝对没有性能差异,将此类测试排除在循环之外也是一个好习惯。一个更好的习惯是使用@pmr 所说的标准算法,这完全回避了这个问题。

        【讨论】:

        • 即使编译器不能证明end 的值是循环不变的,提升它仍然可能不会更快。原因只是即使它被提升了,该值仍然必须存储在一个变量中,该变量很可能在堆栈上。容器也可能在堆栈上,在这种情况下,从容器中的某些数据成员中读取其end 可能与从变量中读取提升的end 完全相同。如果容器是通过引用传递的,那么可能会有一个额外的间接,并且可能会稍微慢一些。
        • @SteveJessop:感谢您富有洞察力的评论,我更改了措辞,以免留下错误的印象。除此之外,恕我直言,我们(人类)真的不应该如此细细地剖析它,因为很难预测如此小的性能差异。
        • 我同意我们无法预测这些微小的差异。我真的不同意提升这样的测试是一个好习惯。对于某些人来说,它可能会提高可读性(2 行较短而不是 1 行较长),否则这是一种推测性的优化,并不值得为 IMO 弄乱代码。如果提升机降低了整个操作的复杂性,或者如果它显然会成为循环中完成的工作的很大一部分,那么我会投机地去做。否则我不会,尽管我也不反对在使用算法或基于范围的 for 时“免费”获取它。
        • 感谢您的回答:由于 cmets 还包含非常好的信息(感谢史蒂夫),所以我选择了这个。事后阅读 Steves 的笔记:因为对于 C++11 来说,在常量迭代器上擦除是可能的(在我看来这非常好),如果后来有人决定修改循环内的容器,将 end 放在循环之外可能会引入错误。虽然这不太可能发生,但它仍然是可以避免的可能错误来源
        • 我认为这是一个好习惯的一个原因是它隐含地声明了循环的不变量。这通常是我将它作为变量提升的原因——它是一些文档(对于我自己和其他人),集合的大小预计不会在循环过程中发生变化。它可能稍微更优化是次要的。相反,如果我不使用这个成语,则暗示大小可能会发生变化。
        【解决方案5】:

        其实 end() 方法是内联的。第二个不是每次都调用它,我不认为 end() 有任何性能滞后。

        【讨论】:

          【解决方案6】:

          std::vector.end() (for example) 按值返回一个迭代器。在第二个循环中,您在每个循环中创建一个对象。编码标准告诉您不要在不需要时创建对象。编译器可能很聪明,会为您优化代码,但这并不能保证。更好的解决方案是使用 stl 算法。它们已经过优化,您的代码将更具可读性。请注意,仅当您不修改集合时,这两个循环才等效。

          附:很可能性能差异非常小

          【讨论】:

          • end() 的调用可能是内联的,对operator!= 的调用也是如此。在那个级别,我们正在比较两个指针。拥有其中一个的单独副本不太可能提高性能。
          • 我同意最终的性能结果不会有很大不同,但是我喜欢关注对象扩散的想法。
          【解决方案7】:

          对于现在阅读本文的任何人来说,这个问题在 C++11 中已成为一个有争议的问题。

          我不确定这个回答是否可以作为答案,因为它实际上并没有解决问题的重点。但我确实认为指出这里提出的问题对于 C++11 程序员来说在实践中很少遇到是有道理的,而且几年前我肯定会发现这个响应很有用。因此,此回复的目标读者是只想了解迭代 STL 容器中所有元素的最佳方法(vectorlistdeque 等)。

          假设 OP 想要访问容器中的每个元素,我们可以通过编写 range-based for loop 轻松回避定义 end 是否比调用 Container::end() 足够快的整个问题:

          Container container; // my STL container that has been filled with stuff
          
          // (note that you can replace Container::value_type with the value in the container)
          
          // the standard way
          for (Container::value_type element : container) {
              // access each element by 'element' rather than by '*it'
          }
          
          // or, if Container::value_type is large
          Container container; // fill it with something
          for (Container::value_type& element : container) {
              //
          }
          
          // if you're lazy
          Container container; // fill it with something
          for (auto element : container) {
              //
          }
          

          OP 已询问在每次迭代中简单地比较 itContainer::end() 的简洁性与声明变量 end 并在每一步进行比较的性能之间的权衡是否值得。由于基于范围的 for 循环提供了一个简单、易于编写和易于阅读的替代方案,它也恰好在内部声明一个 end 迭代器,而不是在每一步调用 Container::end() 方法,因此我们需要的案例数量详述这个问题已减少到数量有限的案例。

          根据cppreference.com,基于范围的 for 循环将生成具有与以下相同副作用的代码:

          {
            auto && __range = range_expression ; 
            for (auto __begin = begin_expr,
                  __end = end_expr; 
                __begin != __end; ++__begin) { 
              range_declaration = *__begin; 
              loop_statement 
            } 
          } 
          

          【讨论】:

            猜你喜欢
            • 2020-06-24
            • 1970-01-01
            • 2010-12-15
            • 2013-06-16
            • 2011-03-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多