为什么在c++中deque比vector使用更多的内存?

visual Why is deque using so much more RAM than vector in C++?

本文关键字:内存 vector c++ deque 为什么      更新时间:2023-10-16

我有一个问题,我正在工作的地方,我需要使用某种二维数组。数组的宽度是固定的(四列),但是我需要动态地创建额外的行。

为了做到这一点,我一直在使用向量的向量,并且我一直在使用一些嵌套循环,其中包含以下内容:

array.push_back(vector<float>(4));
array[n][0] = a;
array[n][1] = b;
array[n][2] = c;
array[n][3] = d;
n++

来添加行及其内容。问题是,我试图创建的元素数量似乎用光了内存,所以我减少了使用的元素数量。但后来我开始了解deque,我认为它可以让我使用更多的内存,因为它不必是连续的。在这个循环中,我将所有提到的"vector"改为"deque",以及所有声明。但是,似乎我又耗尽了内存,这一次,即使减少了的行数。

我查看了我的代码使用了多少内存,当我使用deque时,内存稳定地上升到2GB以上,并且程序很快关闭,即使使用较少的行数也是如此。当内存耗尽时,我不确定它在这个循环中的确切位置。

当我使用向量时,即使在循环退出时,内存使用量(对于相同数量的行)仍然低于1GB。然后进入一个类似的循环,添加更多的行,仍然只有大约1.4GB。

我的问题是。deque使用vector的两倍以上的内存是正常的吗?还是我在认为我可以在声明/初始化和上述代码中用"deque"替换"vector"这个词时做出了错误的假设?

提前感谢。

我用:MS Visual c++ 2010(32位)Windows 7(64位)

真正的答案与核心数据结构关系不大。答案是MSVC对std::deque的实现特别糟糕,并且退化为指向单个元素的指针数组,而不是它应该是的数组的数组。坦率地说,只有两倍的内存使用向量是令人惊讶的。如果你有一个更好的deque实现,你会得到更好的结果。

这完全取决于deque的内部实现(我不会谈论vector,因为它相对简单)。

事实是,dequevector具有完全不同的保证(最重要的是它支持两端O(1)插入,而vector只支持后部O(1)插入)。这反过来意味着deque管理的内部结构必须比vector更复杂。

为了实现这一点,典型的deque实现将其内存分割为几个不连续的块。但是每个单独的内存块都有一个固定的开销来允许内存管理工作(例如。无论块的大小如何,系统可能需要另外的16或32字节或任何额外的东西,只是为了记账)。因为,与vector相反,deque需要许多小的、独立的块,开销堆积起来,这可以解释您看到的差异。还要注意,这些单独的内存块需要管理(可能在单独的结构中?),这可能也意味着一些(或很多)额外的开销。

至于解决问题的方法,你可以尝试@BasileStarynkevitch在评论中建议的方法,这确实会减少你的内存使用,但它只能让你到目前为止,因为在某些时候你仍然会耗尽内存。如果您试图在一台只有256MB RAM的机器上运行您的程序怎么办?任何其他的解决方案,其目标是减少内存占用,同时仍然试图保持所有的数据在内存中会遭受同样的问题。

当处理像你这样的大型数据集时,一个适当的解决方案是调整你的算法和数据结构,以便能够处理整个数据集的小分区,并根据需要加载/保存这些分区,以便为其他分区腾出空间。不幸的是,因为这可能意味着磁盘访问,这也意味着性能的大幅下降,但是嘿,你不能鱼和熊掌兼得。

理论


有两种常见的方法可以有效地实现deque:要么使用修改后的动态数组,要么使用双链表

修改后的动态数组使用的基本上是一个可以从两端增长的动态数组,有时称为数组deques。这些数组deques具有动态数组的所有属性,例如恒定时间随机访问,良好的引用局部性,中间插入/删除效率低,并且在两端(而不是一端)增加了平摊的恒定时间插入/删除。

有几种修改动态数组的实现:

  1. 从底层数组的中心分配deque内容,并在到达任意一端时调整基础数组的大小。这方法可能需要更频繁地调整大小,并且浪费更多的空间

  2. 将队列内容存储在循环缓冲区中,仅在以下情况下调整大小缓冲区满了。

  3. 将内容存储在多个较小的数组中,分配额外的数组的开头或结尾。索引是通过保持一个动态数组,其中包含指向每个较小数组的指针数组。

结论


不同的库可以以不同的方式实现deque,但通常是作为修改的动态数组。您的标准库很可能使用方法#1来实现std::deque,而由于您只从一端附加元素,因此最终浪费了大量空间。因此,它会产生一种错觉,认为std::deque比通常的std::vector占用更多的空间。

此外,如果std::deque将实现为双链表,这也会导致空间浪费,因为每个元素除了您的自定义数据之外还需要容纳2个指针。

方法#3的实现(也修改了动态数组方法)将再次导致空间浪费,以容纳额外的元数据,如指向所有这些小数组的指针。

在任何情况下,std::deque在存储方面的效率都低于普通的旧std::vector。在不知道您想要实现什么目标的情况下,我无法自信地建议您需要哪种数据结构。然而,似乎你甚至不知道deques是用来做什么的,因此,你真正想要的是std::vector。deque通常有不同的应用。

Deque比vector有额外的内存开销,因为它是由几个块而不是连续的块组成的。

从en.cppreference.com/w/cpp/container/deque:

std::vector相反,deque的元素不是连续存储的:典型的实现是使用一系列单独分配的固定大小的数组。

主要问题是内存不足。

那么,您需要一次在内存中存储所有数据吗?
你可能永远无法做到这一点。

部分处理

你可能想考虑将数据处理成"块"或更小的子矩阵。例如,使用标准矩形网格:

  • 读取第一象限数据。
  • 处理第一象限数据。
  • 存储第一象限的结果(在文件中)
  • 重复剩余象限。

搜索

如果你正在搜索一个粒子或一组数据,你可以不把整个数据集读入内存。

  1. 分配内存块(数组)
  2. 将一部分数据读入该内存块。
  3. 查找数据块
  4. 重复步骤2和3,直到找到数据。

流数据

如果您的应用程序从输入源(而不是文件)接收原始数据,您将希望存储这些数据以供以后处理。

这将需要多个缓冲区,并且使用至少两个执行线程会更有效率。

读线程将一直读取数据到缓冲区,直到缓冲区满为止。当缓冲区已满时,它将把数据读入另一个空缓冲区。

写线程将首先等待,直到第一个读缓冲区已满或读操作完成。接下来,写线程从读缓冲区中取出数据并写入文件。然后写线程从下一个读缓冲区开始写。

这种技术称为双重缓冲或多重缓冲。

稀疏数据

如果矩阵中有很多零数据或未使用的数据,您应该尝试使用稀疏矩阵。本质上,这是一个保存数据坐标和值的结构体列表。当大多数数据是除零以外的公共值时,这也适用。这节省了大量的内存空间;但是会花费更多的执行时间。

数据压缩

你也可以改变你的算法来使用数据压缩。这里的想法是存储数据位置,值和连续相等值的数量(又名运行)。因此,不是存储相同值的100个连续数据点,而是存储(运行的)起始位置、值和100作为数量。这节省了大量空间,但在访问数据时需要更多的处理时间。

内存映射文件

有一些库可以将文件视为内存。从本质上讲,它们将文件的"一页"读入内存。当请求离开"页面"时,它们会读入另一个页面。所有这些都是在"幕后"进行的。你所需要做的就是像对待内存一样对待这个文件。

总结

数组和队列不是你的主要问题,数据量才是。您的主要问题可以通过每次处理小块数据、压缩数据存储或将文件中的数据视为内存来解决。如果您正在尝试处理流数据,请不要这样做。理想情况下,流数据应该放在文件中,然后再进行处理。文件的历史目的是包含内存无法容纳的数据。