为什么 std::vector 不保留 "double" 其容量,而 resize 保留?

Why does std::vector reserve not "double" its capacity, while resize does?

我刚刚发现 std::vector<T>::resize "doubles" 即使调整到比当前大小高出一个元素,它的容量也是如此:

std::vector<int> v(50);
v.resize(51);
std::cout << v.capacity() << std::endl;

此程序使用 GCC 和 Clang 输出 100,使用 Visual C++ 输出 75。但是,当我从 resize 切换到 reserve:

std::vector<int> v(50);
v.reserve(51);
std::cout << v.capacity() << std::endl;

所有三个编译器的输出都是 51。

我想知道为什么实现对 resizereserve 使用不同的扩展策略。看起来不一致,我希望这里有相同的行为。


我只是在我的问题的动机中添加了一个 link,其中报告了对性能的影响:


添加来自 C++11 标准的引用以阐明 reserve 的要求; §23.3.6.3(2):

After reserve(), capacity() is greater or equal to the argument of reserve if reallocation happens...


一些额外的想法:来自 C++11 标准:

Complexity: The complexity is linear in the number of elements inserted plus the distance to the end of the vector.

这实际上意味着在末尾插入单个元素的复杂性恒定(摊销)。但是,这仅适用于 矢量修饰符 ,例如 push_backinsert (§23.3.6.5).

resize 未在修饰符中列出。它列在 §23.3.6.3 vector 容量部分。而且,resize.

没有复杂性要求

但是,在 vector 概述部分 (§23.3.6.1) 中,写着:

it (vector) supports (amortized) constant time insert and erase operations at the end

问题是resize(size()+1)算不算"insertion at the end"

当您 resize 超过容量时,您已经 "demonstrate" 不想保留正确的容量。另一方面,如果您使用 reserve 则您明确要求正确的容量。如果 reserve 将使用与 resize 相同的策略,则无法保留恰到好处的数量。

从这个意义上说,没有 reserveresize 是为懒惰的人准备的,或者如果您不知道要保留的确切金额。如果您知道需要什么容量,您可以致电 reserve。这是两种不同的情况。

PS:正如 StoryTeller 指出的那样,reserve 也不需要按照标准保留要求的确切数量。尽管如此,我认为我的主要论点仍然成立:resize(没有 reserve)和 reserve 用于不同的场景,您可以暗示您想要保留多少或不不关心实际容量,只想让容器的大小符合您的要求。

reserve改变容量,而resize改变size

capacity是容器当前已经分配space的元素个数。

size是容器中的元素个数。

当你分配一个空向量时,你会得到一个默认值 capacity(又名 space)。大小仍然为 0,当您将元素添加到向量中时,它的大小会增加。当大小等于容量并且您添加更多项目时,容量必须增加(通常是自身的两倍)。

vector 的问题在于它确保顺序内存,这意味着每个新的分配增长也需要将先前分配的副本复制到新分配,以防新分配大小没有 space在旧分配的内存区域中。

此处 reserve 可以提供帮助,如果您知道向量中的最大元素。当你使用reserve时,只有一次分配,没有内存复制,除非你传递保留项。

当您指定准确的预留计数时,您将获得您要求的准确内存。当你只是添加元素时(即使调整大小),你不会说你不会添加更多项目。

据我所知,resizereserve 都不需要具有已证明的行为。然而,两者都允许这样的行为,尽管两者都可以分配确切的数量,并且就标准而言,两者都可以乘以先前的分配。

每种分配策略都有其优势。分配精确数量的优点是,当事先知道最大分配量时,它没有内存开销。乘法的优点是它在与末插入操作混合时保持恒定的摊销属性。

被测试的实现选择的方法的优点是它在调整大小时允许两种策略。要使用一种策略,可以保留然后调整大小。要使用另一个,只需调整大小。当然,必须意识到未指定的行为才能利用这一点。这个优势可能是也可能不是选择这些实现的原因。

人们可能会认为这是向量 API 的失败,正如标准中所指定的那样,表达预期的重新分配行为是不可能的(以标准保证的方式)。

您为什么期望他们的行为相同? reserve 用于预分配 space 您稍后将使用,期望用户对容器的预期最终大小有一个合适的处理。 resize 只是一个普通的分配,因此它遵循正常的、速度高效的方法,以几何方式增加容器的分配 space。

容器通过乘法步骤增加大小,以减少所需的分配数量,从而保持速度并减少内存碎片。加倍是最常见的,但一些实现使用 1.5 的步骤(例如 MSVC),以增加分配来换取每个容器中较低的浪费 space。

但是,如果用户已经告诉图书馆他们认为容器会变得多大 - 通过导致 reserve - 没有必要分配多余的 space,他们可以信任用户用正确的号码打电话给它。有异常行为的是 reserve,而不是 resize

resize 需要遵循指数重新分配策略来实现其复杂性保证(与插入的元素数量 呈线性关系)。这可以通过考虑 resize(size() + 1) 需要具有摊销常数复杂性来看出,因此必须遵循指数增长,原因与 push_back(摊销常数复杂性)必须呈指数增长相同。

允许 reserve 的实现遵循它喜欢的任何分配策略,因为它唯一的复杂性要求是它在元素数量 present 上是线性的。但是,如果一个实现是例如要舍入到下一个 2 的幂,这将是 space-在用户确切知道需要多少内存的情况下效率低下(并且令人惊讶),并且如果用户开始依赖它可能会使移植复杂化行为。在没有 space 低效率的情况下,可以更好地行使标准中的自由度,例如如果分配器以该粒度运行,则通过将分配向上舍入到字大小。