【问题标题】:std::vector increasing peak memorystd::vector 增加峰值内存
【发布时间】:2019-08-08 04:30:57
【问题描述】:

这是我上一个问题的延续。我无法理解向量占用的内存。问题骨架:

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

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::lists 的大小来计算。

现在,由于有 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。这对向量来说太多了。

大家愿意纠正我在这里无法理解的地方吗?

附:我无法在较小的情况下重现它,它即将出现在我的项目中。

【问题讨论】:

  • std::list 要求是支持双向迭代器。这意味着列表节点必须至少存储两个指针,加上您的数据,这些数据本身可能在单独的代理中分配。诸如列表节点之类的小分配也会对需要额外内存进行核算的内存子系统造成负担。你考虑过这些吗?
  • @Chipster,实际上您也可以将其视为一个独立的问题。我的最后一个 qn 与类似的向量有关,但与自动销毁有关。请把它当作一个独立的。
  • 注意:内存在被程序释放时并不总是返回给系统。有时运行时会保留它,以便以后不必再次请求它。有时在系统需要它之前它不会给予任何回报。因此,您需要小心使用哪些工具来查看程序内存。
  • @Chipster 啊,但是在我(半)可以使用一屋子 Cray XD-1 的日子里。好时光,好时光。
  • 一个 std::list 大约是一个 std::list 太多了,你有六亿...

标签: c++ vector


【解决方案1】:
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&lt;ABC*&gt; 对象。因此,您应该期望向量本身的缓冲区使用vec.capacity() * sizeof(std::list&lt;ABC*&gt;) 字节。每个列表至少有一个指向开始和结束的指针。

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

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

此外,由于链表的每个元素都是单独分配的,并且每次动态分配都需要记账以便它们可以单独解除分配,并且每次分配都必须与最大本地对齐方式对齐,并且免费存储在执行过程中可能已经碎片化,每次动态分配都会有很多开销。

嗯,清除这个向量后,内存下降到 100G。

语言实现保留从操作系统分配的(一些)内存是非常典型的。如果您的目标系统记录了明确请求释放此类内存的实现特定功能,那么您可以尝试使用它。

但是,如果向量缓冲区不是最新的动态分配,则其释放可能会在空闲存储中留下大量可重用区域,但如果存在后续分配,则所有内存可能无法释放回操作系统。

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


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

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

【讨论】:

  • 非常感谢您的回复。我希望我能接受这两个答案。但是,赞成你。 :) 让我问你和 robbthebloke 一样的问题:你建议如何处理这种情况?
  • @RahulBhargava 通常,为了减少相同字节大小的许多分配的碎片,您可以使用内存池。例如,Boost provides some memory-pooling aware allocators。这些可用于分配链表节点和ABC 对象。 MP 不仅减少了所需的内存量,而且还可以显着加快分配本身的速度(我们在哈希表的 HPC 代码中使用这种方法,其中每个桶都是一个链表)。
【解决方案2】:

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

因此,使用std::list&lt;ABC*&gt; 的示例,指针的大小是 8,但是您将需要至少 48 字节来存储每个元素(至少)。

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

向量中的 std::lists 大小约为 18GB。所以总的来说,我们至少要考虑 180Gb 来存储该向量。对于所有这些单独的内存分配(例如加载的内存页面列表、换出的内存页面列表、读/写/ mmap 权限、等等等等)。

最后一点,您可以使用收缩来适应,而不是在新构建的向量上使用交换。

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

【讨论】:

  • 感谢您如此出色的回复。但是,我不明白为什么存储每个元素至少需要 48 个字节?另外,是否有任何 api 可以给我这个向量使用的整体内存?哪个也计算内部?
  • 每个列表节点包含:2 个指针(上一个/下一个),以及您正在存储的数据(在本例中为另一个指针)。所以总共是 24 字节(3 x 8)。然而,操作系统在返回内存之前会有一个 16 字节的分配头(所以现在我们是 40 字节)。在 64 位操作系统上,返回的分配需要 16 字节对齐,由于 40 与 16 字节对齐,分配需要另外填充 8 个字节。
  • 对。我明白。有道理。但是,您建议如何处理这种情况?
  • 基本上,不要使用 std::list! :p 如果内存使用是一个主要问题,那么 std::vector 通常是更好的选择。
  • 请记住,vector 的调整大小策略也可能有点麻烦。如果您需要的条目超出预期和保留,您可能会发现您的内存成本大幅增加(虽然通常是 50-100%,而不是 500%,所以这里最坏的情况仍然是一个巨大的胜利)
【解决方案3】:

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

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

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

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

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

【讨论】:

  • vec.at(index) 是std::list&lt;ABC*&gt;&amp;,因此它是std::back_inserter 的有效参数
  • 该死。你完全正确。我现在必须完全重新考虑答案的那一部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-08
  • 1970-01-01
  • 2011-01-19
  • 1970-01-01
  • 2018-10-23
  • 2015-01-25
  • 1970-01-01
相关资源
最近更新 更多