C++中向量的初始容量

Posted

技术标签:

【中文标题】C++中向量的初始容量【英文标题】:Initial capacity of vector in C++ 【发布时间】:2012-08-29 13:23:15 【问题描述】:

使用默认构造函数创建的std::vectorcapacity() 是什么?我知道size() 为零。我们可以声明一个默认构造的向量不调用堆内存分配吗?

这样就可以使用单个分配创建具有任意保留的数组,例如std::vector<int> iv; iv.reserve(2345);。假设由于某种原因,我不想在 2345 上启动 size()

例如,在 Linux 上(g++ 4.4.5,内核 2.6.32 amd64)

#include <iostream>
#include <vector>

int main()

  using namespace std;
  cout << vector<int>().capacity() << "," << vector<int>(10).capacity() << endl;
  return 0;

打印0,10。这是一个规则,还是依赖于 STL 供应商?

【问题讨论】:

标准没有指定向量的初始容量,但大多数实现使用 0 。 无法保证,但我会严重质疑任何在我没有请求的情况下分配内存的实现的质量。 @MikeSeymour 不同意。一个真正高性能的实现可能包含一个小的内联缓冲区,在这种情况下,将初始容量()设置为这个值是有意义的。 @alastair 当使用swap 时,所有迭代器和引用都保持有效(end()s 除外)。这意味着内联缓冲区是不可能的。 【参考方案1】:

该标准没有指定容器的初始 capacity 应该是什么,因此您依赖于实现。一个常见的实现将从零开始容量,但不能保证。另一方面,没有办法改进你的std::vector&lt;int&gt; iv; iv.reserve(2345); 策略,所以坚持下去。

【讨论】:

我不买你最后的声明。如果您最初不能依赖容量为 0,则可以重组程序以允许向量具有初始大小。这将使堆内存请求的数量减半(从 2 到 1)。 @bitmask:实用:您知道 any 实现,其中向量在默认构造函数中分配内存吗?标准不能保证这一点,但正如 Mike Seymour 指出的那样,在不需要的情况下触发分配对于实施质量来说是一种不好的气味。 @DavidRodríguez-dribeas:这不是重点。前提是“你不能比你当前的策略做得更好,所以不要担心是否有可能愚蠢的实现”。如果前提是“没有这样的实现,所以不要打扰”我会买它。结论恰好是正确的,但暗示不起作用。对不起,也许我在挑剔。 @bitmask 如果存在在默认构造时分配内存的实现,那么按照您所说的操作将分配数量减半。但是vector::reserve 与指定初始大小不同。采用初始大小值/副本的向量构造函数初始化n 对象,因此具有线性复杂度。 OTOH,调用reserve 仅意味着复制/移动size() 元素如果 触发了重新分配。在空向量上没有可复制的内容。因此,即使实现为默认构造的向量分配内存,后者也可能是可取的。 @bitmask,如果您担心这种程度的分配,那么您应该查看特定标准库的实现,而不是依赖推测。【参考方案2】:

std::vector 的存储实现差异很大,但我遇到的都是从 0 开始的。

以下代码:

#include <iostream>
#include <vector>

int main()

  using namespace std;
  
  vector<int> normal;
  cout << normal.capacity() << endl;
  
  for (unsigned int loop = 0; loop != 10; ++loop)
  
      normal.push_back(1);
      cout << normal.capacity() << endl;
  
  
  cin.get();
  return 0;

给出以下输出:

0
1
2
4
4
8
8
8
8
16
16

在 GCC 5.1、11.2 - Clang 12.0.1 和:

0
1
2
3
4
6
6
9
9
9
13

在 MSVC 2013 下。

【讨论】:

这被低估了@Andrew 嗯,你几乎到处都发现,出于速度目的的建议几乎总是只使用向量,所以如果你正在做任何涉及稀疏数据的事情...... @Andrew 他们应该从什么开始?如果程序员想要保留比默认更多的内存,那么分配任何东西只会浪费时间分配和释放内存。如果您假设它们应该以 1 开头,那么只要有人分配 1,它就会立即分配它。 @Puddle 您在字里行间阅读,而不是从表面上看。这不是讽刺的线索是“聪明”这个词,以及我提到稀疏数据的第二条评论。 @Andrew 哦,太好了,他们从 0 开始就放心了。为什么还要开玩笑地评论它?【参考方案3】:

就我所理解的标准而言(尽管我实际上无法命名参考),容器实例化和内存分配已被有意解耦,这是有充分理由的。因此,您有不同的、单独的要求

constructor 创建容器本身 reserve() 预先分配一个适当大的内存块以容纳至少(!)给定数量的对象

这很有意义。 reserve() 存在的唯一权利是让您有机会在增长向量时围绕可能昂贵的重新分配进行编码。为了有用,您必须知道要存储的对象数量,或者至少需要能够做出有根据的猜测。如果不这样做,您最好远离reserve(),因为您只会更改浪费内存的重新分配。

所以把它们放在一起:

该标准有意指定允许您为特定数量的对象预分配内存块的构造函数(这至少比分配实现特定的、固定的“某些东西”)。 分配不应是隐式的。因此,要预先分配一个块,您需要单独调用reserve(),这不需要在同一个施工地点(当然可以/应该在以后,在您意识到需要容纳的大小之后)李> 因此,如果一个向量总是预先分配一个实现定义大小的内存块,这会破坏reserve() 的预期工作,不是吗? 如果 STL 自然无法知道向量的预期用途和预期大小,那么预分配块的优势是什么?如果没有适得其反的话,那将是相当荒谬的。 正确的解决方案是使用第一个 push_back() 分配和实现特定块 - 如果之前 reserve() 尚未明确分配。 在必要的重新分配情况下,块大小的增加也是特定于实现的。我所知道的向量实现从大小呈指数增长开始,但会将增量速率限制在某个最大值,以避免浪费大量内存甚至破坏它。

只有在不受分配构造函数干扰的情况下,所有这些才能充分发挥作用和优势。对于常见场景,您有合理的默认值,reserve()(和shrink_to_fit())可以按需覆盖这些默认值。因此,即使标准没有明确说明,我很确定假设新构造的向量不预分配对于所有当前实现来说是一个相当安全的选择。

【讨论】:

【参考方案4】:

作为对其他答案的一点补充,我发现在使用 Visual Studio 在调试条件下运行时,即使容量从零开始,默认构造的向量仍将在堆上分配。

具体来说,如果 _ITERATOR_DEBUG_LEVEL != 0 那么向量将分配一些空间来帮助迭代器检查。

https://docs.microsoft.com/en-gb/cpp/standard-library/iterator-debug-level

我只是觉得这有点烦人,因为我当时使用的是自定义分配器,并没有期待额外的分配。

【讨论】:

有趣的是,他们打破了 noexcept 保证(至少对于 C+17,更早?):en.cppreference.com/w/cpp/container/vector/vector【参考方案5】:

这是一个老问题,这里的所有答案都正确地解释了标准的观点以及使用std::vector::reserve 以可移植方式获得初始容量的方式;

但是,我将解释为什么 在构造 std::vector&lt;T&gt; 对象时分配内存对任何 STL 实现都没有意义;

    std::vector&lt;T&gt; 的类型不完整;

    在 C++17 之前,如果 T 的定义在实例化时仍然未知,则构造 std::vector&lt;T&gt; 是未定义的行为。 However, that constraint was relaxed in C++17。

    为了有效地为对象分配内存,您需要知道它的大小。从 C++17 及更高版本开始,您的客户可能会遇到 std::vector&lt;T&gt; 类不知道 T 大小的情况。依赖于类型完整性的内存分配特征有意义吗?

    Unwanted Memory allocations

    您需要在软件中为图形建模很多很多次。 (树是图);您最有可能将其建模为:

    class Node 
        ....
        std::vector<Node> children; //or std::vector< *some pointer type* > children;
        ....
     ;
    

    现在想一想,如果您有很多终端节点。如果你的 STL 实现只是为了在 children 中有对象而分配额外的内存,你会非常生气。

    这只是一个例子,请随意考虑更多......

【讨论】:

【参考方案6】:

标准没有指定容量的初始值,但 STL 容器会自动增长以容纳尽可能多的数据,前提是您不超过最大大小(使用 max_size 成员函数知道)。 对于向量和字符串,当需要更多空间时,增长由 realloc 处理。假设您想创建一个持有值 1-1000 的向量。如果不使用保留,代码通常会导致介于 在以下循环中进行 2 次和 18 次重新分配:

vector<int> v;
for ( int i = 1; i <= 1000; i++) v.push_back(i);

修改代码以使用保留可能会导致循环期间分配为 0:

vector<int> v;
v.reserve(1000);

for ( int i = 1; i <= 1000; i++) v.push_back(i);

粗略地说,向量和字符串的容量每次增长 1.5 到 2 倍。

【讨论】:

以上是关于C++中向量的初始容量的主要内容,如果未能解决你的问题,请参考以下文章

为啥在达到容量后在向量中进行插入时,C++ 不处理迭代器?

C++ priority_queue 底层向量容器容量调整大小

使用 Enumerable.Repeat 进行容量列表初始化的问题 [重复]

`resize` 是不是有降低向量容量的风险?

C++初始化模板类

Java 之 Vector 集合