【问题标题】:Performance issue for vector::size() in a loop in C++C++ 循环中 vector::size() 的性能问题
【发布时间】:2011-04-23 12:45:21
【问题描述】:

在以下代码中:

std::vector<int> var;
for (int i = 0; i < var.size(); i++);

size() 成员函数是为每个循环迭代调用,还是只调用一次?

【问题讨论】:

  • 您是否测量了差异或查看了输出?
  • 对不起,我不知道如何衡量它有我可以阅读的参考吗?或搜索关键字
  • 应该注意的是,使用 std 算法有助于编译器优化,因为它们将循环代码与范围的生成分开。 std::for_each(var.begin(), var.end(), Action());

标签: c++ performance vector for-loop stdvector


【解决方案1】:

您的问题的问题在于它没有任何意义。 C++ 编译器将一些源代码翻译成二进制程序。要求是生成的程序必须根据 C++ 标准的规则保留代码的可观察效果。这段代码:

for (int i = 0; i < var.size(); i++); 

只是没有任何可观察到的效果。此外,它不会与周围的代码进行任何交互,编译器可能会将其完全优化掉;也就是不生成对应的程序集。

为了使您的问题有意义,您需要指定循环内发生的情况。问题

for (int i = 0; i < var.size(); i++) { ... }

答案很大程度上取决于... 实际上是什么。我相信@MatteoItalia 提供了一个非常好的答案,只是会添加对我所做的一些实验的描述。考虑以下代码:

int g(std::vector<int>&, size_t);

int f(std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0; i < v.size(); i++)
      res += g(v, i);
   return res;
}

首先,即使调用 var.size() 几乎 100% 肯定会内联启用优化,并且这种内联通常转化为两个指针的减法,这仍然会给循环带来一些开销。如果编译器不能证明向量大小被保留(这通常是非常困难甚至不可行的,例如在我们的例子中),那么你最终会得到不必要的 load 和 sub(可能还有shift)指令。使用 GCC 9.2、-O3 和 x64 生成的循环程序集是:

.L3:
    mov     rsi, rbx
    mov     rdi, rbp
    add     rbx, 1
    call    g(std::vector<int, std::allocator<int> >&, unsigned long)
    add     r12d, eax
    mov     rax, QWORD PTR [rbp+8] // loads a pointer
    sub     rax, QWORD PTR [rbp+0] // subtracts another poniter
    sar     rax, 2                 // result * sizeof(int) => size()
    cmp     rbx, rax
    jb      .L3

如果我们重写代码如下:

int g(std::vector<int>&, size_t);

int f(std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0, e = v.size(); i < e; i++)
      res += g(v, i);
   return res;
}

然后,生成的程序集更简单(因此更快):

.L3:
    mov     rsi, rbx
    mov     rdi, r13
    add     rbx, 1
    call    g(std::vector<int, std::allocator<int> >&, unsigned long)
    add     r12d, eax
    cmp     rbx, rbp
    jne     .L3

向量大小的值简单地保存在寄存器中 (rbp)。

我什至尝试了一个不同的版本,其中向量被标记为const

int g(const std::vector<int>&, size_t);

int f(const std::vector<int>& v) {
   int res = 0;
   for (size_t i = 0; i < v.size(); i++)
      res += g(v, i);
   return res;
}

令人惊讶的是,即使v.size() 无法在此处更改,生成的程序集与第一种情况相同(附加了movsubsar 指令)。

现场演示是here

另外,当我把循环改成:

for (size_t i = 0; i < v.size(); i++)
   res += v[i];

然后,在汇编级别的循环内没有对v.size()(指针减法)的评估。 GCC 能够在这里“看到”循环体不会以任何方式改变大小。

【讨论】:

    【解决方案2】:

    对其进行了 90 万次迭代测试 预先计算大小需要 43 秒,使用 size() 调用需要 42 秒。

    如果您保证向量大小在循环中不会改变,最好使用预先计算的大小,否则别无选择,必须使用 size()。

    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
    vector<int> v;
    
    for (int i = 0; i < 30000; i++)
            v.push_back(i);
    
    const size_t v_size = v.size();
    for(int i = 0; i < v_size; i++)
            for(int j = 0; j < v_size; j++)
                    cout << "";
    
    //for(int i = 0; i < v.size(); i++)
    //      for(int j = 0; j < v.size(); j++)
    //              cout << "";
    }
    

    【讨论】:

      【解决方案3】:

      理论上,每次都会调用,因为一个for循环:

      for(initialization; condition; increment)
          body;
      

      扩展为类似

      {
          initialization;
          while(condition)
          {
              body;
              increment;
          }
      }
      

      (注意花括号,因为初始化已经在内部范围内)

      在实践中,如果编译器了解您的条件在整个循环期间是不变的并且它没有副作用,它可以聪明地把它搬出去。这通常使用strlen 和类似的东西(编译器很清楚)在没有写入其参数的循环中完成。

      但是必须注意,最后一个条件并不总是容易证明的;一般来说,如果容器是函数的本地容器并且从不传递给外部函数,这很容易;如果容器不是本地的(例如它是通过引用传递的——即使它是const)并且循环体包含对其他函数的调用,编译器通常必须假设这些函数可能会改变它,从而阻止长度的提升计算。

      如果你知道你的条件的一部分是“昂贵的”评估是值得的(而且这种条件通常不是,因为它通常归结为指针减法,这几乎肯定是内联的) .


      编辑: 正如其他人所说,通常对于容器,最好使用迭代器,但对于 vectors 而言,它并不那么重要,因为可以保证通过 operator[] 随机访问元素O(1);实际上对于向量,它通常是指针总和(向量基数+索引)和解引用与指针 increment(前一个元素+1)和迭代器的解引用。由于目标地址仍然相同,我认为您不能从迭代器中获得缓存位置方面的东西(即使是这样,如果您不是在紧密循环中遍历大数组,您甚至不应该注意到这样某种改进)。

      对于列表和其他容器,使用迭代器代替随机访问可能真的很重要,因为使用随机访问可能意味着每次遍历列表时都会遍历,而递增迭代器只是指针取消引用.

      【讨论】:

      • 其实……增量不应该在while循环里面吗?
      • “如果您通过 const 引用操作向量,编译器可以利用此信息来确保其字段永远不会改变”。除非向量对象本身(不仅仅是引用)是 const。如果您调用可能通过别名修改向量的代码,那么即使 your 引用为 const,编译也无法优化。如果你不调用未知代码,那么即使你的引用是非常量的,编译器也可以优化。
      • 使用 size_t 代替 vector::size_type 因为通过引用它需要相同,而后者将在 C++17 中弃用
      • @user3063349:实际上,我将完全删除该段,因为当我写这个答案时,我改变了将size_t 作为索引的想法 - 一般来说,它只是比它的价值更麻烦。
      • @MatteoItalia 你所说的“更麻烦”是什么意思,你想说我们应该使用 int 来代替吗?
      【解决方案4】:

      认为如果编译器可以最终推断出变量 var 在“循环体”内没有被修改

      for(int i=0; i< var.size();i++) { 
          // loop body
      }
      

      那么上面的内容可以转换成等价的东西

      const size_t var_size = var.size();
      for( int i = 0; i < var_size; i++ ) { 
          // loop body
      }
      

      但是,我不确定,所以欢迎 cmets :)

      还有,

      • 在大多数情况下,size() 成员函数是内联的,所以这个问题不需要担心

      • 这个问题可能同样适用于end(),它总是用于基于迭代器的循环,即it != container.end()

      • 请考虑将size_tvector&lt;int&gt;::size_type 用于i 的类型[参见下方史蒂夫·杰索普的评论。]

      【讨论】:

      • std::vector&lt;int&gt;::size() 的返回类型是std::vector&lt;int&gt;::size_type,您强烈希望它与size_t 的类型相同,但不一定。
      【解决方案5】:

      正如其他人所说,编译器应决定如何处理实际编写的代码。关键是它每次都被调用。但是,如果您想获得性能提升,最好在编写代码时考虑一些因素。你的情况就是其中之一,还有其他的,比如这两段代码的区别:

      for (int i = 0 ; i < n ; ++i)
      {
         for ( int j = 0 ; j < n ; ++j)
             printf("%d ", arr[i][j]);
         printf("\n");
      }
      for (int j = 0 ; j < n ; ++j)
      {
         for ( int i = 0 ; i < n ; ++i)
             printf("%d ", arr[i][j]);
         printf("\n");
      }
      

      不同的是,第一个不会对每个引用更改太多的 ram 页面,但另一个会耗尽你的缓存和 TLB 和其他东西。

      inline 也没有多大帮助!因为调用函数的顺序将保持为 n(向量的大小)次。虽然它在某些地方有所帮助,但最好的办法是重写你的代码。

      但是!如果你想让编译器对你的代码进行优化,永远不要放 volatile,像这样:

      for(volatile int i = 0 ; i < 100; ++i)
      

      它会阻止编译器进行优化。 如果您需要其他性能提示,请使用 register 而不是 volatile。

      for(register int i = 0 ; i < 100; ++i)
      

      编译器会尽量不将 i 从 CPU 寄存器移动到 RAM。不能保证它可以做到,但它会做到最好;)

      【讨论】:

      • 我想不出任何编译器实际上会考虑寄存器...编译器会自己选择寄存器。
      • 当然内联会有所帮助...因为它可能会内联到 size 成员变量,因此没有函数调用...
      • 另外,即使您对缓存位置的看法是正确的......它与所提出的问题无关......
      • @ronag:我想这是错误的想法,我没有说内联没有帮助,我只是说重写代码更好。此外,编译器选择是否内联函数也是如此。我只是这样回答他的问题,因为我认为他很好奇如何使 for 循环更好。
      • 重写代码如何更好?任何体面的编译器都会在这些微优化方面做出比我们任何人都更好的决定。
      【解决方案6】:

      size() 成员函数每次都会被调用,但如果不将其内联,那将是一个非常糟糕的实现,而且它不是简单访问固定数据或减去两个指针。
      无论如何,在您分析您的应用程序并发现这是一个瓶颈之前,您不应该担心这些琐碎的事情。

      但是,您应该注意的是:

      1. 向量索引的正确类型是std::vector&lt;T&gt;::size_type
      2. 有些类型(例如一些迭代器)i++可能++i慢。

      因此,循环应该是:

      for(vector<int>::size_type i=0; i<var.size(); ++i)
        ...
      

      【讨论】:

        【解决方案7】:

        但是可以通过这种方式完成(假设这个循环只打算读/写而不实际改变向量的大小):

        for(vector<int>::size_type i=0, size = var.size(); i < size; ++i) 
        {
        //do something
        }
        

        在上面的循环中,您只需调用一次 size 就可以独立于 size 是否内联。

        【讨论】:

          【解决方案8】:

          正如其他人所说的

          • 语义必须如同每次都被调用
          • 它可能是内联的,并且可能是一个简单的函数

          在此之上

          • 一个足够聪明的优化器可能能够推断出它是一个没有副作用的循环不变量并完全忽略它(如果代码是内联的,这会更容易,但即使它不是 if 编译器进行全局优化)

          【讨论】:

            【解决方案9】:

            必须每次都调用它,因为 size() 每次都可能返回不同的值。

            因此,它必须是没有什么大的选择。

            【讨论】:

            • 这个答案在最一般意义上是正确的(生成的代码必须表现得好像每次都被调用),但是编译器编写者工作非常 很难发现可以安全排除的特殊情况。
            • 是的 ;-) 但是你不能依赖它,因为这是编译器特定的。
            【解决方案10】:

            它每次都被“调用”,但我把调用放在引号中,因为它很可能只是一个内联方法调用,所以你不必担心它的性能。

            为什么不改用vector&lt;int&gt;::iterator

            【讨论】:

            • "vector::iterator" 比 "int" 详细得多——没有提供任何实际价值。尽管如所写,OP 可能会收到带有 int 与 vector::size_type 的有符号/无符号比较警告。
            • @nobar:我认为迭代器在零缺点的情况下提供了巨大的好处。很抱歉让您觉得输入几个字符是一种负担。由于整个 STL 是基于迭代器学习正确使用它们是一个nessesty。
            • @Martin:C++ 标准委员会也很抱歉,这就是为什么他们在 C++0x 中提供了基于范围的 for 来替代 for_each 和其他非常简单的许多情况算法。除了我认为他们的同情更真诚;-p
            • @Steve Jessop:不是在争论,而是提到为什么他们认为迭代器不好?我将迭代器视为向前迈出的一步(不是完美的一步,而是一步)。经过十年的使用,我们现在更好地理解了问题,并且更好地理解了迭代器的优势(和劣势)。范围概念只是构建在迭代器之上的另一个步骤,我不认为它们是替代品,我认为它们是基本概念的扩展。
            • @Martin:我并不是说他们已经放弃了迭代器,只是他们真的很抱歉输入几个字符是一种负担。基于范围的 for 几乎只是为了减少代码,无论您之前使用的是等效循环还是等效的 for_each。当然,Alexandrescu 确实特别认为迭代器“某事、某处出现了严重错误”,并且范围应该取代这个概念,而不是扩展它。但从某种意义上说,他还是一个 C++“逃兵”。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-09-06
            相关资源
            最近更新 更多