在C ++中使用数组或std :: vectors,性能差距是多少?


208

在我们的C ++课程中,他们建议不再在新项目上使用C ++数组。据我所知,Stroustroup本人建议不要使用数组。但是,性能存在明显差异吗?


2
您为什么会认为存在性能差距。
马丁·约克

99
因为通常情况下,更好的功能会带来最差的性能。
tunnuz

19
我同意过早的优化,但是预先选择更好的存储方法很有意义。通常,在现实世界中,需要交付代码并开发下一个产品,并且永远不会发生优化步骤。
2012年

132
我希望人们不再尖叫“过早的优化”!每当有人问与性能相关的简单问题时!回答问题,而不仅仅是假定人们在做任何过早的事情。
d7samurai 2014年

4
@ d7samaurai:同意,我还没有看到任何人尝试使用int main(int argc, const std::vector<string>& argv)
Mark K Cowan

Answers:


189

new应避免将C ++数组与(即使用动态数组)一起使用。您必须跟踪大小,这是一个问题,您需要手动删除它们并进行各种整理。

不建议在堆栈上使用数组,因为您没有范围检查,并且将数组传递给周围将丢失任何有关其大小的信息(数组到指针转换)。boost::array在这种情况下,应使用将C ++数组包装在一个小类中,并提供一个size函数和迭代器对其进行迭代的情况。

现在 std :: vector与本机C ++数组(从互联网上获取):

// Comparison of assembly code generated for basic indexing, dereferencing, 
// and increment operations on vectors and arrays/pointers.

// Assembly code was generated by gcc 4.1.0 invoked with  g++ -O3 -S  on a 
// x86_64-suse-linux machine.

#include <vector>

struct S
{
  int padding;

  std::vector<int> v;
  int * p;
  std::vector<int>::iterator i;
};

int pointer_index (S & s) { return s.p[3]; }
  // movq    32(%rdi), %rax
  // movl    12(%rax), %eax
  // ret

int vector_index (S & s) { return s.v[3]; }
  // movq    8(%rdi), %rax
  // movl    12(%rax), %eax
  // ret

// Conclusion: Indexing a vector is the same damn thing as indexing a pointer.

int pointer_deref (S & s) { return *s.p; }
  // movq    32(%rdi), %rax
  // movl    (%rax), %eax
  // ret

int iterator_deref (S & s) { return *s.i; }
  // movq    40(%rdi), %rax
  // movl    (%rax), %eax
  // ret

// Conclusion: Dereferencing a vector iterator is the same damn thing 
// as dereferencing a pointer.

void pointer_increment (S & s) { ++s.p; }
  // addq    $4, 32(%rdi)
  // ret

void iterator_increment (S & s) { ++s.i; }
  // addq    $4, 40(%rdi)
  // ret

// Conclusion: Incrementing a vector iterator is the same damn thing as 
// incrementing a pointer.

注意:如果使用分配数组new并分配非类对象(例如plain int)或不使用用户定义的构造函数的类,并且您不希望初始初始化元素,则使用new-allocated数组会带来性能优势,因为std::vector将所有元素初始化为构造上的默认值(例如,int为0)(@ bernie提醒我)。


77
谁发明了该死的AT&T语法?只要我知道... :)
Mehrdad Afshari,

4
对于Visual C ++编译器而言,情况并非如此。但是对于海湾合作委员会来说。
TOTO

5
在我的回答的一点是,载体不具有比correponding指针运算慢。当然,它可以(也可以通过启用启用调试模式来轻松实现):)
Johannes Schaub-litb

18
+1为“索引向量与索引指针是同一件事”。以及其他结论。
纳瓦兹

3
@ Piotr99我不会与您争论,但是当您学习了高级语言之后学习汇编语言时,Intel语法的含义比一些向后的,前缀的(数字),后缀的(指令)和晦涩的(访问内存)要有意义得多。 )的性质。
科尔·约翰逊

73

微型优化人员的序言

记得:

“程序员浪费大量时间来思考或担心程序的非关键部分的速度,而在考虑调试和维护时,这些对效率的尝试实际上会产生严重的负面影响。我们应该忘记效率低下,例如, 97%的时间:过早的优化是万恶之源。但是,我们不应该在这3%的临界风险中放弃机会。”

(感谢变态的完整报价)

不要仅仅使用C数组而不是向量(或其他任何向量),因为您认为它会更快,因为它应该是较低级别的。你会错的。

默认情况下使用矢量(或适合您需要的安全容器),然后,如果您的分析器认为这是一个问题,请查看是否可以通过使用更好的算法或更改容器来对其进行优化。

话虽如此,我们可以回到原来的问题。

静态/动态数组?

C ++数组类比低级C数组的行为更好,因为它们对自己有很多了解,并且可以回答C数组无法回答的问题。他们能够自己清洗。更重要的是,它们通常是使用模板和/或内联语言编写的,这意味着在调试中许多代码中出现的内容可以解析为发行版本中生成的代码很少或根本没有生成,这意味着它们与内置的安全性较低的竞争没有区别。

总而言之,它分为两类:

动态数组

使用指向malloc-ed / new-ed数组的指针最多和std :: vector版本一样快,而且安全性要差很多(请参阅litb的文章))。

因此,请使用std :: vector。

静态数组

最好使用静态数组:

  • std :: array一样快版本
  • 而且安全性要差很多。

所以使用std :: array

未初始化的内存

有时,使用a vector代替原始缓冲区会产生明显的成本,因为vector会在构造时初始化缓冲区,而替换的代码却没有初始化,正如bernie回答中指出的那样

如果是这种情况,那么您可以使用a unique_ptr而不是a 来处理它,vector或者,如果这种情况在您的代码行中不是特殊情况,请实际编写一个buffer_owner拥有该内存的类,并为您提供轻松,安全的访问权限,包括诸如调整大小(使用realloc?)或任何您需要的奖金。


1
感谢您也解决静态数组-如果出于性能原因不允许动态分配内存,则std :: vector无效。
汤姆

10
当您说“使用静态数组最多与boost :: array版本一样快”时,它表明您有多偏见。应当相反,Boost:array最多可以像静态数组一样快。
TOTO

3
@toto:这是一个误解:您应该将其读为“最多使用静态数组((与boost :: array版本一样快)&&(安全性要差很多))”。我将编辑帖子以澄清这一点。顺便说一句,谢谢您的怀疑。
paercebal

1
那么std :: array呢?
paulm 2014年

4
始终显示完整报价。“程序员浪费大量时间来思考或担心程序的非关键部分的速度,而在考虑调试和维护时,这些对效率的尝试实际上会产生严重的负面影响。我们应该忘记效率低下, 97%的时间:过早的优化是万恶之源,但我们不应该在这3%的临界风险中放弃机会。” 否则,它变得毫无意义。
变形

32

向量是幕后的数组。表现是一样的。

可能会遇到性能问题的一个地方是,开始时没有正确确定向量的大小。

当向量填充时,它将调整自身大小,这可能意味着要分配新的数组,然后是n个复制构造函数,然后是大约n个析构函数调用,然后是数组删除。

如果构造/销毁很昂贵,那么将向量设为正确的大小会更好。

有一个简单的方法可以证明这一点。创建一个简单的类,显示何时构造/销毁/复制/分配它。创建这些事物的向量,然后开始将它们推到向量的后端。当矢量填充时,随着矢量调整大小,将会有一系列的活动。然后使用向量大小调整为预期元素数的方式再次尝试。您将看到差异。


4
Pendantry:性能具有相同的O. std :: vector进行一点簿记,这大概会花费少量时间。OTOH,您在滚动自己的动态数组时最终要做很多相同的簿记工作。
dmckee ---前主持人小猫

是的我明白。但是,他的问题的重点是性能差异。..我试图解决这个问题。
EvilTeach

如果调用push_back,Gcc的std :: vector确实确实会逐个增加容量。
bjhend 2012年

3
@bjhend那么gcc的std::vector声音听起来不符合标准?我相信该标准要求vector::push_back摊销不变的复杂度,并且push_back在考虑重新分配后,将每个容量的容量增加1 将是n ^ 2复杂度。-假设某种指数型增容push_backinsert,未能reserve将导致至多在矢量内容拷贝常数因子增加。如果您未能达到1.5指数向量增长因子,则意味着约3倍的拷贝数reserve()
Yakk-Adam Nevraumont

3
@bjhend你错了。该标准禁止指数增长:§23.2.3第16段说:“表101列出了为某些类型的序列容器提供的操作,但没有为其他类型的容器提供。实现应为“容器”列中显示的所有容器类型提供这些操作,并且予以实施,以摊销固定时间。” (表101是其中包含push_back的表)。现在请停止传播FUD。没有主流实施违反此要求。微软的标准C ++库增长了1.5倍,而GCC增长了2倍。
R. Martinho Fernandes

27

为了回应梅赫达德所说的话:

但是,在某些情况下您仍然需要数组。当与需要数组的低级代码(即程序集)或旧库接口时,您可能无法使用向量。

完全不对。如果使用以下向量,向量会很好地退化为数组/指针:

vector<double> vector;
vector.push_back(42);

double *array = &(*vector.begin());

// pass the array to whatever low-level code you have

这适用于所有主要的STL实现。在下一个标准中,将要求它工作(即使今天还可以)。


1
当前标准没有这样的规定。它是隐含的,并且实现为连续存储。但是该标准只是说这是一个随机访问容器(使用迭代器)。下一个标准将是明确的。
Frank Krueger

1
&* v.begin()仅将&运算符应用于取消引用迭代器的结果。取消引用可以返回ANY类型。使用地址运算符可以再次返回ANY类型。该标准并未将其定义为指向内存连续区域的指针。
Frank Krueger

15
该标准的原始1998年文本确实不需要该标准,但是2003年有一个附录对此进行了补充,因此该标准确实涵盖了该标准。herbsutter.wordpress.com/2008/04/07/...
维迪奇Trifunovic的

2
C ++ 03明确指出,在大小范围内,&v[n] == &v[0] + n有效n。包含该语句的段落在C ++ 11中未更改。
2012年

2
为什么不只使用std :: vector :: data()?
paulm 2014年

15

在C ++ 11中使用纯数组的原因更少了。

本质上,有3种数组,从最快到最慢,这取决于它们具有的功能(当然,即使对于清单3中的情况3,实现的质量也可以使事情变得非常快):

  1. 静态,大小在编译时已知。---std::array<T, N>
  2. 动态,大小在运行时已知,并且永不调整大小。这里的典型优化是,如果可以直接在堆栈中分配数组。– 不可用。也许dynarray在C ++ 14之后的C ++ TS中。在C中有VLA
  3. 动态且可在运行时调整大小。---std::vector<T>

对于具有固定数量元素的1.纯静态数组,请std::array<T, N>在C ++ 11中使用。

对于2。在运行时指定的固定大小的数组,但是不会改变它们的大小,C ++ 14中进行了讨论,但已移至技术规范并最终由C ++ 14组成。

对于3. std::vector<T> 通常会在堆中请求内存。尽管您可以使用std::vector<T, MyAlloc<T>>自定义分配器来改善这种情况,但这可能会对性能产生影响。T mytype[] = new MyType[n];与之相比,优点是您可以调整其大小,并且不会像普通数组那样衰减到指针。

使用提到的标准库类型可以避免 数组衰减为指针。您将节省调试时间和性能完全相同,如果你使用相同的功能集相同,与普通的数组。


2
std :: dynarray。在审查了对n3690的国家机构的评论之后,该库组件从C ++ 14工作文件中被投票为单独的技术规范。从n3797开始,此容器不是C ++ 14草案的一部分。来自en.cppreference.com/w/cpp/container/dynarray
Mohamed El-Nakib 2014年

1
很好的答案。简要总结,还有更多细节。
Mohamed El-Nakib 2014年

6

使用STL。没有性能损失。这些算法非常有效,并且可以很好地处理我们大多数人不会想到的各种细节。


5

STL是一个高度优化的库。实际上,甚至建议在可能需要高性能的游戏中使用STL。数组太容易出错,无法在日常任务中使用。当今的编译器也非常聪明,可以真正使用STL生成出色的代码。如果您知道自己在做什么,STL通常可以提供必要的性能。例如,通过将向量初始化为所需大小(如果从一开始就知道),就可以基本实现阵列性能。但是,在某些情况下您仍然需要数组。当与需要数组的低级代码(即程序集)或旧库接口时,您可能无法使用向量。


4
考虑到向量是连续的,与需要数组的库进行交互仍然非常容易。
格雷格·罗杰斯

是的,但是如果您想弄乱vector的内部内容,则使用vector的优势会更少。顺便说一句,关键字是“可能不会”。
Mehrdad Afshari

3
我只有一种情况知道不能使用向量:如果大小为0。那么&a [0]或&* a.begin()将不起作用。c ++ 1x将通过引入a.data()函数来解决此问题,该函数返回保留元素的内部缓冲区
Johannes Schaub-litb

我写这本书的时候想到的是基于堆栈的数组。
Mehrdad Afshari

1
向量或任何带有C:的连续容器的接口,vec.data()用于数据和vec.size()大小。就这么简单。
赫尔曼DIAGO

5

关于都力的贡献

结论是整数数组比整数向量快(在我的示例中是5倍)。但是,对于更复杂/未对齐的数据,数组和向量的速度相同。


3

如果以调试模式编译软件,许多编译器将不会内联向量的访问器功能。在性能成为问题的情况下,这将使stl向量的实现慢得多。这也将使代码更易于调试,因为您可以在调试器中看到分配了多少内存。

在优化模式下,我希望stl向量接近数组的效率。这是因为现在已经内联了许多矢量方法。


这一点很重要。对调试STL东西进行性能分析非常非常慢。这也是人们认为STL速度缓慢的原因之一。
Erik Aronesty 2014年

3

std::vector当您想要未初始化的缓冲区(例如,用作的目标memcpy())时,使用vs原始数组绝对会对性能产生影响。一个std::vector将初始化使用默认构造函数的所有元素。原始数组不会。

接受参数(这是第三种形式)的构造函数的C ++规范指出:std:vectorcount

从各种数据源构造一个新的容器,可以选择使用用户提供的分配器alloc。

3)用计数的默认插入的T实例构造容器。不进行任何复制。

复杂

2-3)线性计数

原始数组不会产生此初始化费用。

另请参见如何避免std :: vector <>初始化其所有元素?


2

两者之间的性能差异在很大程度上取决于实现-如果您将实现不佳的std :: vector与最佳数组实现进行比较,则该数组将获胜,但将其转过来,vector将获胜...

只要您将Apple与Apple进行比较(数组和向量都具有固定数量的元素,或者两者都动态调整大小),只要您遵循STL编码惯例,我就会认为性能差异可以忽略不计。别忘了使用标准C ++容器还可以让您使用标准C ++库中的预滚动算法,并且大多数预编译算法的性能可能比自己构建的相同算法的平均实现要好。 。

就是说,恕我直言,矢量在带有调试STL的调试方案中胜出,因为大多数具有适当调试模式的STL实现至少可以突出/显示人们在使用标准容器时所犯的典型错误。

哦,别忘了数组和向量共享相同的内存布局,因此您可以使用向量将数据传递给需要基本数组的传统C或C ++代码。请记住,在这种情况下,大多数赌注都不可用,您将再次处理原始内存。


1
我认为要满足性能要求(O(1)查找和插入),您几乎必须使用动态数组来实现std :: vector <>。当然,这是显而易见的方法。
dmckee ---前主持人小猫

不仅性能要求,而且要求连续存储。错误的向量实现会在数组和API之间放置过多的间接层。一个好的载体的实施将使联的代码,SIMD上循环等使用
最大Lybbert

所描述的错误的矢量实现将不符合该标准。如果要间接,std::deque可以使用。
Phil1970年


1

在某些情况下,内联函数中的内联函数中可以进行矢量访问,超出了编译器内联的范围,它将强制执行函数调用。那将是非常罕见的,以至于不值得担心–总的来说,我会同意litb的观点

令我惊讶的是,现在还没有人提到它-在性能被证明是一个问题之前,不要担心性能,然后再进行基准测试。


1

我认为主要的关注点不是性能,而是安全。数组可能会导致很多错误(例如,考虑调整大小),向量可以为您省去很多麻烦。


1

向量使用的内存比数组多一点,因为它们包含数组的大小。它们还会增加程序的硬盘大小,并可能增加程序的内存占用量。这些增加很小,但是如果您使用的是嵌入式系统,可能会很重要。尽管这些差异很重要的大多数地方是您将使用C而不是C ++的地方。


2
如果这很重要,那么显然您不使用动态大小的数组,因此,您的数组不需要更改大小。(如果他们这样做,您将以某种方式存储大小)。因此,除非我弄错了,否则不妨使用boost :: array,这是什么使您说需要在某个地方“存储大小”?
Arafangion 2010年

1

以下简单测试:

C ++数组与矢量性能测试说明

与“对向量和数组/指针的基本索引,解引用和增量操作生成的汇编代码的比较”的结论相矛盾。

数组和向量之间必须有区别。测试说是这样...只要尝试一下,代码就在里面...


1

有时候,数组确实比向量更好。如果您总是要操纵固定长度的对象集,那么数组会更好。考虑以下代码片段:

int main() {
int v[3];
v[0]=1; v[1]=2;v[2]=3;
int sum;
int starttime=time(NULL);
cout << starttime << endl;
for (int i=0;i<50000;i++)
for (int j=0;j<10000;j++) {
X x(v);
sum+=x.first();
}
int endtime=time(NULL);
cout << endtime << endl;
cout << endtime - starttime << endl;

}

X的向量版本在哪里

class X {
vector<int> vec;
public:
X(const vector<int>& v) {vec = v;}
int first() { return vec[0];}
};

X的数组版本是:

class X {
int f[3];

public:
X(int a[]) {f[0]=a[0]; f[1]=a[1];f[2]=a[2];}
int first() { return f[0];}
};

main()的数组版本将更快,因为我们每次都避免在内部循环中使用“ new”的开销。

(此代码由我发布到comp.lang.c ++)。


1

如果您使用矢量来表示多维行为,则会对性能造成影响。

2d +向量会导致性能下降吗?

要点是每个子向量都具有大小信息会产生少量开销,并且不必进行数据序列化(多维c数组也是如此)。缺少序列化可以提供比微优化更多的机会。如果您要处理多维数组,最好扩展std :: vector并滚动自己的get / set / resize bits函数。


0

假设使用固定长度的数组(例如,int* v = new int[1000];vs std::vector<int> v(1000);,大小v固定为1000),那么真正重要的性能考虑因素(或者至少在我处于类似困境时对我而言至关重要)是访问内存的速度。元件。我查找了STL的矢量代码,这是我发现的内容:

const_reference
operator[](size_type __n) const
{ return *(this->_M_impl._M_start + __n); }

肯定会由编译器内联此函数。因此,只要您打算做的唯一事情就是使用v来访问其元素operator[],那么似乎性能上就应该没有任何区别。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.