Effective STL [14] | 使用reserve来避免不必要的重新分配

警告
本文最后更新于 2023-08-03,文中内容可能已过时。

自动扩容

STL 容器只要存储的对象不超过「最大大小」,就可以自动增长到足以容纳放进去的数据。这个最大值,只要调用名叫max_size的成员函数就可以查询到。

对于vector和string,只要需要更多空间,就以realloc等价的思想来增长。

realloc的操作有4个部分:

  1. 分配新的内存块」。在大部分实现中,vector和string的容量每次以「2」为因数增长,即容量每次翻倍。
  2. 把所有元素从容器的旧内存拷贝到新内存」。
  3. 销毁旧内存中的对象」。
  4. 回收旧内存」。 这就是分配,回收,拷贝和析构4个步骤,这些步骤代价都很昂贵。

即便是简单地把一个元素插入vector或string的动作也可能因为需要更新其他使用了指向vector或string中的迭代器、指针或引用的数据结构而膨胀。

4个成员函数

这4个STL容器成员函数,只有vector和string提供了所有这些函数。

成员函数说明
size()「容器中有多少元素」。
没有说明容器为容纳的元素分配了多少内存。
capacity()「容器已经分配的内存中可以容纳多少元素」。
那是容器在那块内存中总共可以容纳多少元素,而不是还可以容纳多少元素。如果想知道一个vector或string中有多少没有被占用的内存,必须从capacity()中减去size()。如果size和capacity返回同样的值,容器中就没有剩余空间了,而下一次插入(通过insert或push_back等)会引发上面的重新分配步骤。
resize(Container::size_type n)
「强制把容器改为容纳n个元素」。调用resize之后,size将会返回n。如果n小于当前大小,容器尾部的元素会被销毁。如果n大于当前大小,新默认构造的元素会添加到容器尾部。如果n大于当前容量,在元素加入之前会发生重新分配。
reserve(Container::size_type n)「强制容器把它的容量改为至少n,提供的n不小于当前大小」。
这一般强迫进行一次重新分配,因为容量需要增加。

reserve成员函数允许你最小化必须进行的重新分配的次数,因而可以避免真分配的开销和迭代器/指针/引用失效。

调用reserve不改变容器中对象的个数。

提前 reserve

只要有元素需要插入而且容器的容量不足时就会发生重新分配」(包括它们维护的「原始内存分配和回收」,「对象的拷贝和析构」和「迭代器、指针和引用的失效」)。

「避免重新分配的关键」是使用reserve尽快把容器的容量设置为足够大,最好在容器被构造之后立刻进行。

Example

假定你想建立一个容纳1-1000值的vector<int>。没有使用reserve

1
2
3
4
vector<int> v;
for (int i = 1; i <= 1000; ++i) {
  v.push_back(i);
}

在大多数STL实现中,这段代码在循环过程中「将会导致2到10次重新分配」。(「vector在重新分配时一般把容量翻倍」,$1000 \approx 2^{10}$。) 把代码改为使用reserve

1
2
3
4
5
vector<int> v;
v.reserve(1000);
for (int i = 1; i <= 1000; ++i) {
  v.push_back(i);
}

这在循环中不会发生重新分配。

结论

通常有2种情况使用reserve来避免不必要的重新分配:

  1. 可用的情况是「当你确切或者大约知道有多少元素将最后出现在容器中」。可以提前reserve适当数量的空间。

2.「保留可能需要的最大的空间」,然后,一旦添加完全部数据「修整掉任何多余的容量」。

Buy me a coffee~
支付宝
微信
0%