在C ++循环中vector :: size()的性能问题

时间:2010-10-10 18:37:14

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

在以下代码中:

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

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

10 个答案:

答案 0 :(得分:43)

理论上,每次调用它,因为for循环:

for(initialization; condition; increment)
    body;

扩展为类似

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

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

在实践中,如果编译器理解你的一部分条件在循环的整个持续时间内都是不变的而且它没有副作用,它可以要聪明才能把它搬出去。这通常是使用strlen和类似的东西(编译器都知道)在没有写入其参数的循环中完成的。

然而必须指出的是,最后一个条件并不总是微不足道的证明;通常,如果容器是函数的本地容器并且永远不会传递给外部函数,则很容易;如果容器不是本地的(例如它通过引用传递 - 即使它是const)并且循环体包含对其他函数的调用,编译器通常必须假设这些函数可能会改变它,从而阻止了长度计算。

如果你知道你的条件的一部分“昂贵”来评估(并且这种条件通常不是,因为它通常归结为指针减法,几乎肯定是内联的),那么手工进行优化是值得的。


编辑:正如其他人所说,通常使用容器最好使用迭代器,但对于vector来说它并不那么重要,因为通过operator[]随机访问元素保证是O(1);实际上,对于向量,它通常是指针求和(向量基数+索引)和取消引用对指针增量(在元素+ 1之前)和迭代器的解除引用。由于目标地址仍然相同,我不认为你可以从缓存局部性方面获得迭代器的东西(即使如此,如果你没有在紧密循环中走大数组,你甚至不应该注意到一种改进)。

对于列表和其他容器,使用迭代器而不是随机访问可能确实很重要,因为使用随机访问可能意味着每次遍历列表,而递增迭代器只是一个指针取消引用

答案 1 :(得分:5)

每次都“调用”,但是我将调用调用为引号,因为它实际上可能只是一个内联方法调用,所以你不必担心它的性能。

为什么不使用vector<int>::iterator

答案 2 :(得分:5)

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

但是,应该注意的是:

  1. 矢量索引的正确类型为std::vector<T>::size_type
  2. 有些类型(例如某些迭代器)i++ 可能++i慢。
  3. 因此,循环应该是:

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

答案 3 :(得分:2)

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

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

答案 4 :(得分:1)

正如其他人所说的

  • 语义必须像每次调用一样
  • 它可能是内联的,可能是一个简单的功能

在其上面

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

答案 5 :(得分:1)

认为如果编译器可以最终推断变量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
}

但是,我并不十分确定,欢迎提出意见:)

此外,

  • 在大多数情况下,size()成员函数都是内联的,因此问题无法令人担忧

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

  • 请考虑使用size_tvector<int>::size_type作为i的类型[请参阅下面的Steve Jessop评论。]

答案 6 :(得分:0)

但是可以这样做(假设这个循环只打算读/写而不实际改变向量的大小):

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

在上面的循环中,您只需调用一个大小,与内联大小无关。

答案 7 :(得分:0)

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

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。它无法确保它可以做到,但它会做到最好;)

答案 8 :(得分:0)

测试了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 << "";
}

答案 9 :(得分:0)

您的问题是,这没有任何意义。 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()几乎可以确保启用的优化都内联,并且这种内联通常会转化为两个指针的减法,但这仍然会给循环带来一些开销。如果编译器无法证明保留了向量大小(通常,这很困难,甚至不可行,例如在我们的例子中),那么您将得到不必要的 load 和< em> 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能够在此处“看到”该循环的主体不会以任何方式更改大小。