【问题标题】:Are const_iterators faster?const_iterators 更快吗?
【发布时间】:2010-10-19 19:31:18
【问题描述】:

我们的编码指南更喜欢const_iterator,因为与普通的iterator 相比,它们要快一些。当您使用const_iterator 时,编译器似乎优化了代码。

这真的正确吗?如果是,那么内部究竟发生了什么让const_iterator 更快?

编辑:我写了一个小测试来检查 const_iteratoriterator 并发现不同的结果:

对于 10,000 个对象 const_terator 的迭代减少了几毫秒(大约 16 毫秒)。但并非总是。有一些迭代是相等的。

【问题讨论】:

  • 在你的测量中,你测量了墙上的时间吗?
  • 是的。该代码看起来类似于@Neil Butterworth 发布的代码。我使用 GetTickCount() 进行时间测量
  • 在进行测试时,您应该考虑可能存在的问题,例如缓存,这很容易使首次运行的测试变慢,但甚至可以使其更快(如果您碰巧将容器的元素填充得更近到begin() 最后)。让程序设置数据,对每个迭代器进行一次传递(丢弃那些时间),然后对每个迭代器进行大量传递并报告结果是一个好主意)。最小值比平均值更有意义。确保通道没有被优化(例如,使用迭代器来接触一些 volatile 变量)。
  • 16 毫秒太小,意义不大。运行测试 5-10 秒以获得一些可比较的结果。

标签: c++ stl iterator const-iterator


【解决方案1】:

如果没有别的,const_iterator 读起来更好,因为它告诉任何阅读代码的人“我只是在迭代这个容器,而不是弄乱包含的对象”。

这是一场伟大的胜利,别介意任何性能差异。

【讨论】:

  • 而且在任何情况下,const_iterator 都不会执行 worse。正面你赢,反面你不输。
  • 虽然没有回答这个问题,是吗?
【解决方案2】:

我们使用的准则是:

总是更喜欢 const 而不是 non-const

如果你倾向于使用 const 对象,你会习惯于只对你得到的对象使用常量操作,那就是尽可能地使用 const_iterator

常量有一个 viral 属性。一旦你开始使用它,它就会传播到你的所有代码。您的非变异方法变为常量,并且只需要对属性使用常量操作,并传递常量引用,这本身只强制执行常量操作...

对我来说,使用常量迭代器相对于非常量迭代器(如果有的话)的性能优势远不如代码本身的改进重要。意味着(设计)为非变异的操作恒定的。

【讨论】:

    【解决方案3】:

    它们适用于重要的容器/迭代器。养成正确的习惯,在重要的时候你就不会失去表现。

    此外,无论如何,有几个原因更喜欢 const_iterator:

    1. 使用 const 表达代码意图(即只读,不改变这些对象)。
    2. 使用 const(_iterator) 可防止意外修改数据。 (见上文)
    3. 一些库使用缺少常量 begin() 将数据标记为脏数据(即 OpenSG),并会在同步时将其发送到其他线程/通过网络,因此它会对性能产生实际影响。
    4. 此外,允许您访问非常量成员函数可能会产生您不希望的副作用(与上述方式大致相同),例如从共享数据中分离写时复制容器。 Qt 就是这样做的。

    作为上面最后一点的一个例子,下面是 Qt 中 qmap.h 的摘录:

    inline iterator begin() { detach(); return iterator(e->forward[0]); }
    inline const_iterator begin() const { return const_iterator(e->forward[0]); }
    

    即使 iterator 和 const_iterator 实际上是等价的(const 除外), detach() 如果有两个或更多对象使用它,则创建数据的新副本。如果您只是要读取数据(使用const_iterator 表示),这完全没用。

    当然,还有另一个方向的数据点:

    1. 对于 STL 容器和许多简单复制语义容器,性能无关紧要。代码等价的。但是,能够编写清晰的代码并避免错误会胜出。
    2. Const 具有病毒性,因此,如果您使用的是 const 实现不佳(或根本没有)实现的遗留代码库,则可能必须使用非 const 迭代器。
    3. 显然,一些 C++0x 之前的 STL 存在一个错误,即无法使用 const_iterators 从容器中擦除()元素。

    【讨论】:

      【解决方案4】:

      我不明白为什么会这样 - constness 是编译时检查。但显而易见的答案是编写测试。

      编辑:这是我的测试 - 它在我的机器上给出了相同的时间:

      #include <vector>
      #include <iostream>
      #include <ctime>
      using namespace std;;
      
      
      int main() {
          vector <int> v;
          const int BIG = 10000000;
          for ( int i = 0; i < BIG; i++ ) {
              v.push_back( i );
          }
          cout << "begin\n";
          int n = 0;
          time_t now = time(0);
          for ( int a = 0; a < 10; a++ ) {
              for( vector <int>::iterator it = v.begin(); it != v.end(); ++it ) {
                  n += *it;
              }
          }
          cout << time(0) - now << "\n";
          now = time(0);
          for ( int a = 0; a < 10; a++ ) {
              for( vector <int>::const_iterator cit = v.begin(); cit != v.end(); ++cit ) {
                  n += *cit;
              }
          }
          cout << time(0) - now << "\n";;
      
          return n != 0;
      
      }
      

      【讨论】:

      • 对于 std::vector 和大多数 STL,这是正确的。对于其他库,情况可能会有所不同。
      【解决方案5】:

      这取决于您使用的容器和实现。

      是的,const_iterator可能表现更好。

      对于某些容器,常量迭代器和可变迭代器的实现可能不同。我能想到的第一个例子是SGI's STL rope container。为了支持更新,可变迭代器有额外的指向父绳索的指针。这意味着浪费了额外的资源用于引用计数更新 + 指向父绳索的指针的内存。请参阅implementation notes here

      但是,正如其他人所说,编译器不能单独使用const 进行优化。 const 只是授予对引用对象的只读访问权限,而不是说它是不可变的。对于像std::vector 这样的容器,其迭代器通常实现为简单的指针,不会有任何区别。

      【讨论】:

      • +1 用于 STL 绳索示例(虽然不是标准的,如果您向非标准容器提出问题,显然任一方向的速度差异都是可能的)。
      • @Tony:一个 C++03 标准示例:string::iterator。对于使用写时复制(在 C++0x 中变得非标准)的实现,可变迭代器意味着检查唯一性,而 const_iterator 不这样做。
      【解决方案6】:

      我们的编码指南说更喜欢 const_iterator

      看看这个article by Scott Meyers here。他解释了为什么人们应该更喜欢迭代器而不是 const_iterator。

      【讨论】:

      • 虽然很有趣,但速度不是那篇文章的论据。
      • 那是一篇相当老的文章,早在 2001 年和 2003 年标准之前。我不知道作者是否仍有意见,我的猜测是他没有。
      • IMO 迈耶斯错了。他基本上是在争辩说,由于您不能将 const_iterator 转换为迭代器,因此不能通过 const_iterator 进行更改,因此您应该更喜欢迭代器。但事实上这就是为什么你应该使用 const_iterator -- 来表示你不会尝试通过它进行更改。
      • @John Dibling const,因为你不能更改const 对象......这就是重点。
      • 文章已过时。擦除/插入非常量迭代器是标准中的一个错误,现已在 C++0x 中修复。
      【解决方案7】:

      它们应该相同,因为 constness 是编译时检查。

      为了向自己证明没有怪癖,我拿了 anon 的代码,修改为使用clock_gettime,添加了一个外部循环以避免缓存偏差,并运行了很多次。结果出人意料地不一致 - 上下浮动 20%(没有可用的空闲框) - 但iteratorconst_iterator最短时间实际上相同。 p>

      然后我让我的编译器 (GCC 4.5.2 -O3) 生成 汇编输出 并在视觉上比较两个循环:相同(除了一对夫妇的顺序寄存器负载被反转)

      iterator循环

          call    clock_gettime
          movl    56(%esp), %esi
          movl    $10, %ecx
          movl    60(%esp), %edx
          .p2align 4,,7
          .p2align 3
      .L35:
          cmpl    %esi, %edx
          je  .L33
          movl    %esi, %eax    .p2align 4,,7
          .p2align 3
      .L34:
          addl    (%eax), %ebx
          addl    $4, %eax
          cmpl    %eax, %edx
          jne .L34
      .L33:
          subl    $1, %ecx
          jne .L35
          leal    68(%esp), %edx
          movl    %edx, 4(%esp)
          leal    56(%esp), %esi
          movl    $1, (%esp)
      

      const_iterator循环:

          movl    60(%esp), %edx
          movl    $10, %ecx
          movl    56(%esp), %esi
          .p2align 4,,7
          .p2align 3
      .L38:
          cmpl    %esi, %edx
          je  .L36
          movl    %esi, %eax
          .p2align 4,,7
          .p2align 3
      .L37:
          addl    (%eax), %ebx
          addl    $4, %eax
          cmpl    %eax, %edx
          jne .L37
      .L36:
          subl    $1, %ecx
          jne .L38
          leal    68(%esp), %edx
          movl    %edx, 4(%esp)
          leal    56(%esp), %esi
          movl    $1, (%esp)
      

      【讨论】:

        【解决方案8】:

        当您对其中任何一项进行基准测试时,请确保使用适当的优化级别 - 使用“-O0”与“-O”等会获得截然不同的时序。

        【讨论】:

          【解决方案9】:

          container&lt;T&gt;::const_iterator::operator* 返回 const T&amp; 而不是 T&amp;,因此编译器可以对 const 对象进行通常的优化。

          【讨论】:

          • const 对象没有通常的优化(不在此上下文中)。
          【解决方案10】:

          “常量”与访问限制(公共、受保护、私有)一样,对程序员的好处多于对优化的帮助。
          由于许多原因(例如 const_cast、可变数据成员、指针/引用别名),编译器实际上无法针对涉及 const 的许多情况进行优化。不过,这里最相关的原因是,仅仅因为 const_iterator 不允许修改它所引用的数据,并不意味着不能通过其他方式更改该数据。如果编译器不能确定数据是只读的,那么它就不能真正优化比非常量迭代器的情况。
          更多信息和示例请访问:http://www.gotw.ca/gotw/081.htm

          【讨论】:

            【解决方案11】:

            根据我的经验,编译器在使用 const 迭代器时不会进行任何可衡量的优化。尽管“它可以”的说法是正确的,并且在标准中没有将引用定义为指针。

            但是,您可以对同一个对象有两个引用,因此一个可以是 const,一个可以是非常量。然后,我猜this thread on restrict pointers 中的答案适用:编译器无法知道对象是被另一个线程改变了,还是被一些中断处理代码改变了。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2020-07-31
              • 2017-05-02
              • 1970-01-01
              • 2011-01-12
              • 2011-12-07
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多