标准::矢量增加峰值内存



这是我最后一个问题的延续。我无法理解矢量占用的内存。问题框架:

考虑一个向量,它是列表的集合,列表是指针的集合。完全像:

std::vector<std::list<ABC*> > vec;

其中ABC是我的班级。我们在 64 位机器上工作,因此指针的大小为 8 字节。

在项目中的流程开始时,我将这个向量的大小调整为一个数字,以便我可以在各自的索引中存储列表。

vec.resize(613284686);

此时,矢量的容量和大小将613284686。右。调整大小后,我将列表插入相应的索引,如下所示:

// Some where down in the program, make these lists. Simple push for now.
std::list<ABC*> l1;
l1.push_back(<pointer_to_class_ABC>);
l1.push_back(<pointer_to_class_ABC>);
// Copy the list at location
setInfo(613284686, l1);
void setInfo(uint64_t index, std::list<ABC*> list>) {
std::copy(list.begin(), list.end(), std::back_inserter(vec.at(index));
}

好。所以插入就完成了。值得注意的是:

矢量的大小为 : 613284686 向量中的条目是 : 3638243731//通过遍历向量索引并添加每个索引处 std::list 的大小来计算。

现在,由于指针有3638243731条目,我希望这个向量占用的内存是~30Gb.3638243731 * 8(字节)= ~30Gb。

但是当我在内存中有这些数据时,内存峰值为 400G。

然后我用:

std::vector<std::list<nl_net> >& ccInfo = getVec(); // getVec defined somewhere and return me original vec.
std::vector<std::list<nl_net> >::iterator it = ccInfo.begin();
for(; it != ccInfo.end(); ++it) {
(*it).clear();
}
ccInfo.clear(); // Since it is an reference
std::vector<std::list<nl_net> >().swap(ccInfo); // This makes the capacity of the vector 0.

好吧,在清除此向量后,内存下降到100G。这太像一个载体了。

你们都想纠正我在这里不明白的地方吗?

附言我无法在较小的案例上重现它,它正在我的项目中出现。

vec.resize(613284686);

此时,矢量的容量和大小将613284686

至少是613284686。它可能会更多。

std::vector<std::list<nl_net> >().swap(ccInfo); // This makes the capacity of the vector 0.

从技术上讲,标准不能保证默认构造的向量不会具有 0...但在实践中,这可能是真的。

现在,由于有3638243731个指针条目,我希望这个向量占用的内存是~30Gb。 3638243731 * 8(字节)

但向量不包含指针。它包含std::list<ABC*>对象。因此,您应该期望向量本身的缓冲区使用vec.capacity() * sizeof(std::list<ABC*>)字节。每个列表至少有一个指向开头和结尾的指针。

此外,您应该期望每个列表中的每个元素也使用内存。由于列表是双重链接的,因此您应该期望每个元素大约有两个指针加上数据(第三个指针)的内存。

此外,列表中的每个指针显然都指向一个ABC对象,并且每个指针也使用sizeof(ABC)内存。

此外,由于链表的每个元素都是单独分配的,并且每个动态分配都需要记账,以便可以单独取消分配,并且每个分配必须与最大本机对齐对齐,并且免费存储可能在执行过程中碎片化,因此每个动态分配都会产生很多开销。

好吧,在清除此向量后,内存下降到100G。

语言实现保留从操作系统分配的(一些)内存是很典型的。如果目标系统记录了用于显式请求释放此类内存的实现特定函数,则可以尝试使用该函数。

但是,如果矢量缓冲区不是最新的动态分配,那么它的释放可能会在免费存储中留下大量可重用区域,但如果存在以后的分配,那么所有这些内存可能无法释放回操作系统。

即使语言实现已将内存释放到操作系统,操作系统通常也会为进程映射内存,直到另一个进程实际需要内存用于其他内容。因此,根据您测量内存使用情况的方式,结果可能不一定有意义。


可能有用的一般经验法则:

  • 除非使用全部(或大部分)索引,否则不要使用向量。如果您不这样做,请考虑使用稀疏数组(尽管此类数据结构没有标准容器)。
  • 使用 vector 时,如果您知道分配的上限,请在调整大小之前保留。
  • 没有充分的理由,不要使用链表。
  • 不要依赖从高峰使用中取回所有内存(回到操作系统;内存仍可用于进一步的动态分配)。
  • 不要强调虚拟内存的使用。

std::list 是一个碎片内存容器。通常,每个节点都必须具有它正在存储的数据,加上 2 个上一个/下一个指针,然后您必须在操作系统分配表中添加所需的空间(通常每个分配 16 或 32 字节 - 取决于操作系统)。然后,您必须考虑所有分配都必须在 16 字节边界上返回的事实(无论如何,在基于 Intel/AMD 的 64 位计算机上)。

因此,以std::list<ABC*>为例,指针的大小为 8,但是您至少需要 48 字节来存储每个元素(至少)。

因此,只有列表条目的内存使用量将保持不变:3638243731 * 48(字节)= ~162Gb。 当然,这是假设没有内存碎片(其中可能有 62 字节的可用块,并且操作系统返回整个 62 块而不是请求的 48 个)。我们还假设操作系统的最小分配大小为 48 字节(而不是说 64 字节,这不会太愚蠢,但会推高使用率)。

std::list 本身在向量中的大小约为 18GB。因此,总的来说,我们至少要考虑180Gb来存储该向量。对于所有这些单独的内存分配(例如,加载的内存页列表、交换的内存页列表、读/写/mmap 权限等),额外的分配是额外的操作系统簿记信息,这并不超出可能性。

最后要注意的是,与其在新构建的向量上使用交换,不如使用收缩来适应。

ccInfo.clear();
ccInfo.shrinkToFit();

主向量需要更多考虑。我的印象是它总是固定大小。那么为什么不使用std::array呢?std::vector分配的内存总是比它允许的增长所需的内存多。矢量越大,内存预留就越大,以实现更均匀的增长。背后的原因是将内存中的重定位保持在最低限度。在非常大的矢量上重新定位会占用大量时间,因此会保留大量额外的内存来防止这种情况。

没有可以删除元素(如vector::clear::erase)的向量函数也会释放内存(例如降低容量)。大小会减小,但容量不会减小。同样,这是为了防止搬迁;如果您删除,您也很可能会再次添加。::shrink_to_fit也不能保证您释放了所有已用内存。

接下来是选择用于存储元素的列表。列表真的适用吗?列表在随机访问/插入/删除操作中很强。您是否真的不断地在随机位置向列表中添加和删除 ABC 对象?还是具有不同属性但具有连续内存的另一种容器类型更合适?也许是另一个std::vectorstd::array。如果答案是肯定的,那么您几乎会被列表及其分散的内存分配所困扰。如果不是,则可以通过使用不同的容器类型赢回大量内存。

那么,你真正想做什么呢?您真的需要主容器及其元素的动态增长吗?你真的需要随机操纵吗?或者,您可以对容器和 ABC 对象使用固定大小的数组,并改用迭代?在考虑这一点时,您可能希望阅读可用的容器及其在 en.cppreference.com 上的属性。它将帮助您决定最合适的内容。

*为了好玩,我在VS2017的实现中挖掘了一下,它创建了一个没有增长段的全新向量,复制旧元素,然后将旧向量的内部指针重新分配给新向量,同时删除旧内存。因此,至少使用该编译器,您可以指望释放内存。

最新更新