概述
舍友面试时候遇到的一个有意思的问题,面试官问,如果一个vector我们push了10000个元素进去,然后pop出来9000个,那么它的容量是多少?如何消除多余的容量?
分析
首先,容量大小这个问题,大家一般都知道vector在push元素的时候,如果容量不足以支撑这次的size增加,那么会扩充成原来的两倍,但是缩减的时候呢,如果size降低到了当前的1 / 2,那么会把capacity降低到一半吗?
答案是否定的。最近有点忙,不太有时间仔细看这部分源码,不过实验是表明capacity不会降低的,并且也可以从这几个方面推测不会降低容量的原因:
- 如果用户reserve了vector的容量,然后再pop一部分元素,这时候如果把capacity降低了,那么用户reserve这个行为产生了意义模糊。
- 不方便扩展,因为vector的内存管理并不是直接在vector里面定义的,而是有专门的allocator。
- 极端情况下效率降低。
那么回到刚才这个问题,很容易出现两个错误答案:
- 容量是1024 * 16 = 16384.
- 可以用reserve来降低容量。
首先容量不会真的是16384,不过是个差不多的数字,这个原因是在size本身就很大的情况下按照两倍分配可能会浪费太多的空间,到后面就不是一直两倍来分配了。
其次,reserve是不能用来降低容量的,这一点可以通过简单的实验就可以证明。
正确答案我想有两种:
- 调用vector的
shrink_to_fit
成员函数。 - 假设我们有一个
vector<int> v
,那么调用vector<int>(v).swap(v)
即可。
可行性通过简单的实验就能得证,不过这里有一个问题,这两种方法效率是一样的吗?
首先,第二种方法肯定是出现了剩余所有元素的复制的,而第一种方法则是让我们看看这个函数的源码(在Windows下,MSVC编译器):
_CONSTEXPR20 void shrink_to_fit() { // reduce capacity to size, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
const pointer _Oldlast = _My_data._Mylast;
if (_Oldlast != _My_data._Myend) { // something to do
const pointer _Oldfirst = _My_data._Myfirst;
if (_Oldfirst == _Oldlast) {
_Tidy();
} else {
_Reallocate_exactly(static_cast<size_type>(_Oldlast - _Oldfirst));
}
}
}
咱不得不说,STL这源码真不是给碳基生物看的东西,不过其核心就是最内层的if-else,意思是说,如果size已经是0了,那么全部内存清空,如果size不是0,那么就按照这个size分配合适大小的空间。
那么_Reallocate_exactly
这个函数到底做了什么呢?其源码如下:
_CONSTEXPR20 void _Reallocate_exactly(const size_type _Newcapacity) {
// set capacity to _Newcapacity (without geometric growth), provide strong guarantee
auto& _Al = _Getal();
auto& _My_data = _Mypair._Myval2;
pointer& _Myfirst = _My_data._Myfirst;
pointer& _Mylast = _My_data._Mylast;
const auto _Size = static_cast<size_type>(_Mylast - _Myfirst);
const pointer _Newvec = _Al.allocate(_Newcapacity);
_TRY_BEGIN
if constexpr (is_nothrow_move_constructible_v<_Ty> || !is_copy_constructible_v<_Ty>) {
_Uninitialized_move(_Myfirst, _Mylast, _Newvec, _Al);
} else {
_Uninitialized_copy(_Myfirst, _Mylast, _Newvec, _Al);
}
_CATCH_ALL
_Al.deallocate(_Newvec, _Newcapacity);
_RERAISE;
_CATCH_END
_Change_array(_Newvec, _Size, _Newcapacity);
}
最近实在没时间再深入去看,不过大概可以看出,有这样三个步骤:
- 内存分配:第10行的
const pointer _Newvec = _Al.allocate(_Newcapacity);
。 - 内存拷贝:第13-17行。
- 数组重定位:第23行的
_Change_array(_Newvec, _Size, _Newcapacity);
。
这个过程应该是,先分配一个合适大小的空间,把现有vector元素拷贝过去,然后使现有vector指向这片新内存作为自己的存储空间,然后释放原本的内存。
那么据此推断,前面说的两种做法效率是基本一致的,下面用简单的实验证明:
#include <chrono>
#include <iostream>
#include <vector>
using namespace std;
using namespace chrono;
int main() {
vector<int> t;
for (int i = 0; i < 100000; i++) t.push_back(999);
for (int i = 0; i < 9000; i++) t.pop_back();
auto start = system_clock::now();
// vector<int>(t).swap(t); // way 2
t.shrink_to_fit(); // way 1
auto end = system_clock::now();
auto duration = duration_cast<nanoseconds>(end - start);
cout << static_cast<double>(duration.count()) << endl;
}
在Ubuntu20.04和gcc9.3下编译运行,两种方式时间相差不会超过10%,可以验证结论,这两种方式时间复杂度和实现上的耗时基本一样。