ChatGPT解决这个技术问题 Extra ChatGPT

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

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

为什么你会认为存在性能差距。
因为通常具有更好的功能会带来最差的性能。
我同意过早优化,但预先选择更好的存储方法很有意义。通常在现实世界中,需要交付代码并开发下一个产品,而优化步骤永远不会发生。
我希望人们不要再大喊“过早的优化!”每当有人问一个与性能相关的简单问题时!回答这个问题,不要只是过早地假设人们过早地做任何事情。
@d7samaurai:同意,我还没有看到有人尝试使用 int main(int argc, const std::vector<string>& argv)

E
EddyWD

应避免使用带有 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 分配数组并分配非类对象(如普通 int)或没有用户定义构造函数的类并且您不希望最初初始化您的元素,使用 new 分配的数组可以具有性能优势,因为 std::vector 在构造时将所有元素初始化为默认值(例如,0 表示 int)(感谢@bernie 提醒我)。


谁发明了该死的 AT&T 语法?只有当我知道... :)
请注意,std::tr1::array(或 boost::array)可以解决您将本机数组与 new 一起使用的情况。
这不适用于 Visual C++ 编译器。但对于 GCC 来说是这样。
我回答的重点是向量不必比相应的指针操作慢。当然,也可以(通过启用启用调试模式也很容易实现):)
+1 表示“索引向量与索引指针是一样的该死的事情。”以及其他结论。
C
Community

微优化人员的序言

记住:

“程序员会浪费大量时间去思考或担心程序中非关键部分的速度,而在考虑调试和维护时,这些提高效率的尝试实际上会产生强烈的负面影响。我们应该忘记小的效率,比如97% 的时间:过早的优化是万恶之源。但我们不应该放弃那关键的 3% 的机会”。

(感谢 metamorphosis 的完整报价)

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

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

这就是说,我们可以回到最初的问题。

静态/动态数组?

C++ 数组类比低级 C 数组表现得更好,因为它们对自己了解很多,并且可以回答 C 数组无法回答的问题。他们能够自己清洁。更重要的是,它们通常是使用模板和/或内联编写的,这意味着在调试中出现的大量代码解析为在发布构建中生成的代码很少或没有,这意味着与它们内置的不太安全的竞争没有区别。

总而言之,它分为两类:

动态数组

使用指向 malloc-ed/new-ed 数组的指针充其量与 std::vector 版本一样快,但安全性要低得多(请参阅 litb's post)。

所以使用 std::vector。

静态数组

最好使用静态数组:

与 std::array 版本一样快

而且安全性要低得多。

所以使用 std::array

未初始化的内存

有时,使用 vector 代替原始缓冲区会产生可见的成本,因为 vector 将在构造时初始化缓冲区,而它替换的代码不会,正如他的 answer 中的 bernie 所指出的那样。

如果是这种情况,那么您可以使用 unique_ptr 而不是 vector 来处理它,或者,如果这种情况在您的代码行中并不例外,则实际上编写一个将拥有该内存的类 buffer_owner,并给出您可以轻松安全地访问它,包括调整大小等奖励(使用 realloc?),或任何您需要的东西。


也感谢您处理静态数组 - 如果出于性能原因不允许您动态分配内存, std::vector 将毫无用处。
当您说“使用静态数组充其量与 boost::array 版本一样快”时,它显示了您的偏见程度。应该是相反的,Boost:array 最多可以像静态数组一样快。
@toto:这是一个误解:您应该将其阅读为“使用静态数组充其量是((与 boost::array 版本一样快)&&(安全性要低得多))”。我将编辑帖子以澄清这一点。顺便说一句,谢谢你的怀疑。
std::array 呢?
始终显示完整的报价。 “程序员会浪费大量时间去思考或担心程序中非关键部分的速度,而在考虑调试和维护时,这些提高效率的尝试实际上会产生强烈的负面影响。我们应该忘记小的效率,比如97% 的时间:过早的优化是万恶之源。但我们不应该放弃关键的 3% 的机会。”否则,它会变成毫无意义的声音片段。
E
EvilTeach

向量是引擎盖下的数组。性能是一样的。

您可能会遇到性能问题的一个地方是一开始就没有正确调整向量的大小。

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

如果您的构造/破坏很昂贵,那么最好使向量一开始就具有正确的大小。

有一种简单的方法可以证明这一点。创建一个简单的类,显示它何时被构造/销毁/复制/分配。创建这些东西的向量,并开始将它们推到向量的后端。当向量填满时,随着向量调整大小,将会有一系列活动。然后使用大小为预期元素数量的向量再次尝试。你会看到不同之处。


Pendantry:性能有同样大的 O。std::vector 做了一点点记账,这大概会花费少量时间。 OTOH,在滚动您自己的动态数组时,您最终会做很多相同的簿记。
是的我明白。不过,他的问题的重点是性能差异是什么......我试图解决这个问题。
如果您调用 push_back,Gcc 的 std::vector 确实会一一增加容量。
@bjhend 那么 gcc 的 std::vector 听起来不符合标准?我相信标准要求 vector::push_back 具有摊销常数复杂性,并且在考虑重新分配后,将每个 push_back 的容量增加 1 将是 n^2 复杂性。 -- 假设 push_backinsert 上的某种指数容量增加,reserve 失败最多将导致矢量内容副本的常数因子增加。如果您未能做到 reserve(),则 1.5 指数向量增长因子将意味着大约 3 倍的副本。
@bjhend 你错了。该标准禁止指数增长:第 23.2.3 节第 16 段说“表 101 列出了为某些类型的序列容器提供的操作,而不是其他类型的操作。实现应为“容器”列中显示的所有容器类型提供这些操作,并且应实施它们以采用摊销的恒定时间。” (表 101 是带有 push_back 的表)。现在请停止传播 FUD。没有主流实现违反此要求。 Microsoft 的标准 C++ 库增长了 1.5 倍,而 GCC 增长了 2 倍。
C
Community

回应 Mehrdad 所说的话:

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

一点也不真实。如果您使用,向量会很好地降级为数组/指针:

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

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

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

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


目前的标准没有这样说。它是隐含的,它被实现为连续存储。但标准只是说它是一个随机访问容器(使用迭代器)。下一个标准将是明确的。
1998 年标准的原始文本确实不需要它,但 2003 年有一个附录解决了这个问题,所以它确实被标准涵盖了。 herbsutter.wordpress.com/2008/04/07/…
C++03 明确指出 &v[n] == &v[0] + n 有效,前提是 n 在大小范围内。包含此语句的段落在 C++11 中没有改变。
为什么不直接使用 std::vector::data()?
而另一种方式呢?给定来自低级代码(或 C-Export DLL)的指针,您将无法在不复制的情况下将向量包装在它周围。
G
Germán Diago

在 C++11 中使用普通数组的理由更少。

自然界中有 3 种数组,从最快到最慢,取决于它们所具有的特性(当然,即使对于列表中的案例 3,实现的质量也可以使事情变得非常快):

静态的,在编译时已知大小。 --- std::array 动态的,大小在运行时已知并且从不调整大小。这里的典型优化是,如果数组可以直接在堆栈中分配。 - 无法使用。可能是 C++14 之后 C++ TS 中的 dynarray。在 C 中,VLA 是动态的并且在运行时可调整大小。 --- std::vector

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

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

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

使用提到的标准库类型来避免 arrays decaying to pointers。如果您使用相同的一组功能,您将节省调试时间,并且性能与普通数组完全相同相同。


std::dynarray 。在审查了国家机构对 n3690 的评论后,这个库组件被从 C++14 工作文件中投票否决为单独的技术规范。从 n3797 开始,此容器不是 C++14 草案的一部分。来自en.cppreference.com/w/cpp/container/dynarray
很好的答案。简要和总结,但比任何细节都多。
b
bernie

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

采用 count 参数(它是第三种形式)的 std:vector 构造函数的 c++ spec 状态:

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

使用默认插入的计数 T 实例构造容器。不制作副本。

复杂

2-3) 计数线性

原始数组不会产生这种初始化成本。

请注意,使用自定义分配器,可以避免向量元素的“初始化”(即使用默认初始化而不是值初始化)。有关更多详细信息,请参阅这些问题:

C++11 和 Boost.Container 下 vector::resize(size_type n) 的这种行为是否正确?

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


但这就是为什么我的 small_vector 类有一个 resize 重载,默认构造数据,而不是像所有普通方法那样构造值。
如果您更清楚地区分默认构造与值构造,这个答案会更好。 std::vector总是 值构造,在一些边缘情况下可能会有轻微的开销。在您引用的构造函数位中,向量值构造,尽管暗示它默认构造,这非常烦人。
@MooingDuck 我不会在这里重复已经在很多地方详细解释过的内容。但是,我确实添加了更多信息以表明可以使用自定义分配器来实现默认初始化。
J
John D. Cook

使用 STL。没有性能损失。这些算法非常有效,它们在处理我们大多数人不会想到的各种细节方面做得很好。


m
mmx

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


鉴于向量是连续的,它仍然很容易与需要数组的库交互。
是的,但是如果您想弄乱向量的内部内容,那么使用向量的优势就会减少。顺便说一句,关键词是“可能不会”。
只有一种情况我知道不能使用向量:如果大小为 0。那么 &a[0] 或 &*a.begin() 将不起作用。 c++1x 将通过引入一个 a.data() 函数来解决这个问题,该函数返回保留元素的内部缓冲区
当我写这篇文章时,我脑海中的特定场景是基于堆栈的数组。
使用 C 接口向量或任何连续容器:vec.data() 表示数据,vec.size() 表示大小。就这么容易。
l
lalebarde

About duli's contribution 用我自己的测量结果。

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


佚名

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

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


这一点很重要。分析调试 STL 的东西非常非常慢。这也是人们认为 STL 很慢的原因之一。
T
Timo Geusch

两者之间的性能差异在很大程度上取决于实现——如果你将一个糟糕实现的 std::vector 与一个最佳数组实现进行比较,那么数组会赢,但是把它转过来,向量就会赢……

只要您将苹果与苹果进行比较(数组和向量都有固定数量的元素,或者都动态调整大小),只要您遵循 STL 编码实践,我认为性能差异可以忽略不计。不要忘记,使用标准 C++ 容器还允许您使用作为标准 C++ 库一部分的预滚动算法,并且它们中的大多数可能比您自己构建的相同算法的平均实现性能更好.

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

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


我认为要满足性能要求( O(1) 查找和插入),您几乎必须使用动态数组来实现 std::vector<> 。当然,这是显而易见的方法。
不仅是性能要求,还有连续存储的要求。一个糟糕的向量实现会在数组和 API 之间放置太多的间接层。一个好的向量实现将允许内联代码、循环使用的 SIMD 等。
所描述的错误向量实现将不符合标准。如果您想要间接,可以使用 std::deque
S
Seph Reed

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

Do 2d+ vectors cause a performance hit?

要点是每个具有大小信息的子向量都有少量开销,并且不一定会有数据序列化(就像多维 c 数组一样)。这种缺乏序列化可以提供比微优化更多的机会。如果你在做多维数组,最好只扩展 std::vector 并滚动你自己的 get/set/resize bits 函数。


G
Greg Rogers

如果不需要动态调整大小,则有保存容量的内存开销(一个指针/size_t)。而已。


C
Community

可能存在一些边缘情况,您可以在内联函数内的内联函数内进行向量访问,在这种情况下,您已经超出了编译器将内联的内容,它将强制执行函数调用。这种情况非常罕见,不值得担心——总的来说,我同意 litb

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


G
Gabriel Isenberg

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


B
Brian

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


如果这很重要,那么您显然没有使用动态大小的数组,因此,您的数组不需要更改大小。 (如果他们这样做了,你会以某种方式存储大小)。因此,除非我弄错了,否则您不妨使用 boost::array - 是什么让您说需要在某处“存储大小”?
C
Community

以下简单测试:

C++ Array vs Vector performance test explanation

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

数组和向量之间必须有区别。测试说是这样……试试吧,代码就在那里……


d
duli

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

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];}
};

的数组版本是:

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++)。


O
Obsidian

对于固定长度数组,发布版本中的性能是相同的(与 vector<> 相比),但在调试版本中,根据我的经验(MS Visual Studio 2015,C++ 11),低级数组的优势是 20 倍。

因此,如果您(或您的同事)倾向于在您的数组使用中引入错误,则支持 STL 的“节省时间调试”论点可能是有效的,但如果您的调试时间主要是在等待您的代码运行到您所需要的点,则可能不是目前正在努力,以便您可以逐步完成它。

从事数字密集型代码的经验丰富的开发人员有时会属于第二组(尤其是如果他们使用向量 :))。


S
Subh_b

假设一个固定长度的数组(例如 int* v = new int[1000];std::vector<int> v(1000);v 的大小固定为 1000),唯一真正重要的性能考虑因素(或者至少在我处于类似情况时对我很重要) dilemma) 是访问元素的速度。我查找了 STL 的矢量代码,发现如下:

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

这个函数肯定会被编译器内联。因此,只要您计划对 v 做的唯一事情是使用 operator[] 访问其元素,那么性能似乎应该没有任何差异。