【问题标题】:Why is deque using so much more RAM than vector in C++?为什么在 C++ 中双端队列使用的 RAM 比向量多得多?
【发布时间】:2013-04-27 12:37:31
【问题描述】:

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

为此,我一直在使用向量的向量,并且我一直在使用一些包含以下内容的嵌套循环:

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

添加行及其内容。问题是我试图创建的元素数量似乎已经耗尽了内存,所以我减少了我正在使用的数量。但后来我开始阅读关于双端队列的内容,并认为它可以让我使用更多的内存,因为它不必是连续的。在这个循环中,我将所有提到的“vector”以及所有声明都更改为“deque”。但后来我的内存似乎又用完了,这一次即使行数减少了

我查看了我的代码使用了多少内存,当我使用双端队列时,内存稳步上升到 2GB 以上,并且程序很快关闭,即使使用较少的行数也是如此。我不确定内存不足时该循环的确切位置。

当我使用向量时,即使循环退出,内存使用量(对于相同的行数)仍然低于 1GB。然后它继续进行类似的循环,添加更多行,但仍仅达到约 1.4GB。

所以我的问题是。这是正常的双端队列使用两倍以上的向量内存,还是我在认为我可以在声明/初始化和上述代码中用“双端队列”替换“向量”这个词时做出错误的假设?

提前致谢。

我正在使用: MS Visual C++ 2010(32 位) Windows 7(64 位)

【问题讨论】:

  • 您是否考虑过使用 固定长度 数组的std::vector(使用包含float x[4]; 成员字段的某个类,或者使用std::array,如果有一些C++ 2011 兼容实施)?
  • 你能把SSCCE放在一起吗?
  • @Basile 这样的简单方法至少可以减少一些问题。我应该想到它,但我从未见过它完成,所以我只是没有想到。谢谢:)
  • @BZ1:好吧,另一种解决方案是创建一个具有命名属性的struct...
  • 查看我关于处理大量数据的答案。我不相信您的问题与向量或双端队列有关。无论数据结构如何,您仍然会耗尽内存。

标签: c++ visual-c++ vector ram deque


【解决方案1】:

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

【讨论】:

  • 是时候切换到更好的实施和系统了!您(= 原始发帖人)是否考虑过安装最近的 Linux 并使用一些 GCC 4.8(它完全符合 C++2011,并且有一个不错的标准 C++ 库)。 ... ?
  • 我想指出,讨厌的人,有时您出于各种原因不能切换到其他编译器。话虽如此,MSVC 的标准库很烂。
  • Boost 中有一个。如果您感到绝望,我会建议您这样做。
  • @BasileStarynkevitch 我最初实际安装 Windows 和 VS 的唯一原因是因为我得到了大量代码(用 MSVC 编写),这些代码已经完成了很多我需要做的事情,而我无法让它在任何 linux 编译器上正确编译(不记得我现在尝试了什么,那是不久前的事了)。目前,当我有大量工作要做时,解决任何错误会让人头疼,但如果我有时间进行切换,请记住这一点。干杯。
【解决方案2】:

这一切都取决于deque的内部实现(我不会谈论vector,因为它比较简单)。

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

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

至于解决问题的方法,您可以尝试@BasileStarynkevitch 在 cmets 中建议的方法,这确实会减少您的内存使用量,但它只会让您到目前为止,因为在某些时候您仍然会耗尽内存.如果你试图在只有 256MB RAM 的机器上运行你的程序呢?任何其他旨在减少内存占用同时仍试图将所有数据保留在内存中的解决方案都会遇到同样的问题。

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

【讨论】:

  • 感谢您的回复。使用这些数据很难实现分区。它是原子的位置。在我的代码中的某个时刻,这些被转换为 128^3 体素网格上的电位,所以理论上我可以一次计算一个切片并存储切片,然后再计算下一层的位置,但是有很多原因为什么这很难实现,并且很可能会大大降低性能。我想我会按照@BasileStarynkevitch 的建议去做,并简单地减小我正在建模的粒子的大小以节省 RAM。
  • @BZ1:确实,如果您的数据不适合分区,那么您就有点卡住了。你可以做的是提前reserve你的向量——当然,如果你知道前面的元素数量。这可以减少内存碎片(或者可能不会,我对 Windows 的内存内部管理不够熟悉)并允许您分配稍大的向量。
  • 另外,请记住,如果您只是 push_back 而没有预先保留,则每次重新分配所需的内存是向量实际包含的内存的两倍多。预先保留将消除此要求并允许您分配更多内存(我什至没有说明显的性能提升,因为您不再需要一遍又一遍地复制数据) .
  • 这在理论上是可行的,但完全不切实际。我使用的方法是首先找到一个原子应该在一个完整的晶体中的位置,然后只记录它在对象函数内部的位置(vector&lt;vector&lt;vector&lt;type&gt; &gt; &gt;)。预先计算这个值并不容易,因为我正在对晶体应用旋转和平移,并且对象函数的每个体素中的原子数并不总是相同。这可能是一个很大的错误来源,而我这样做的方式虽然效率低下,但在避免错误方面非常稳健。
  • @BZ1:是的,我意识到我对您的“业务规则”一无所知(我们通常称之为这类东西)。这对我来说太复杂了。但是,如果内存真的是问题,那么您可以以效率进行交易:即使这意味着计算两次,至少您可以将整个东西放入内存中。只是随机的想法,呵呵,我不知道它是否可以体面地适用于你的情况......;)
【解决方案3】:

理论


有两种有效实现双端队列的常用方法:使用修改后的动态数组或使用doubly linked list

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

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

  1. 从底层数组的中心分配双端队列内容, 并在到达任一端时调整底层数组的大小。这 方法可能需要更频繁地调整大小并浪费更多空间, 特别是元素只插入一端时

  2. 将双端队列内容存储在循环缓冲区中,并且仅在 缓冲区变满。这会降低调整大小的频率。

  3. 将内容存储在多个较小的数组中,分配额外的 根据需要将数组放在开头或结尾。索引由以下方式实现 保持一个动态数组,其中包含指向每个较小的指针 数组。

结论


不同的库可能以不同的方式实现双端队列,但通常作为修改后的动态数组。您的标准库很可能使用方法#1 来实现std::deque,并且由于您仅从一端附加元素,最终您浪费了大量空间。出于这个原因,它让人产生一种错觉,即std::deque 占用的空间比平时std::vector 更多。

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

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

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

【讨论】:

    【解决方案4】:

    双端队列可能比向量有额外的内存开销,因为它由几个块而不是连续的块组成。

    来自en.cppreference.com/w/cpp/container/deque

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

    【讨论】:

    • 另一方面,虽然算法在使用vector 时可能会失败,因为无法分配足够大的连续块,但使用deque 将能够成功。
    • @MatthieuM。这确实是一个好点。 deque 往往是一个非常平衡的容器,这就是为什么它默认用于 stackqueue IIRC。
    【解决方案5】:

    主要问题是内存不足。

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

    部分处理

    您可能需要考虑将数据处理成“块”或更小的子矩阵。例如,使用标准矩形网格:

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

    搜索

    如果您正在搜索一个粒子或一组数据,则无需将整个数据集读入内存即可。

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

    流数据

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

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

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

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

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

    稀疏数据

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

    数据压缩

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

    内存映射文件

    有些库可以将文件视为内存。本质上,它们将文件的“页面”读入内存。当请求离开“页面”时,它们会读入另一个页面。所有这一切都是在“幕后”进行的。您需要做的就是将文件视为内存。

    总结

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

    【讨论】:

    • 感谢您的回复。搜索不适用。输入数据只有 128^3,并且该网格内的数十亿个位置是动态创建的,因此流式传输也是 N/A。存储的每个值都是唯一的位置,因此数组中没有零。我可以通过在我的对象函数的每个体素中记录一个点并在我读取数组时创建原子势来使用压缩。我将不得不考虑实现它有多容易。最终,如果需要,我可以将数据保持在最低限度。
    猜你喜欢
    • 2020-04-19
    • 1970-01-01
    • 2011-07-17
    • 2016-09-07
    • 2012-12-03
    • 2018-05-18
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多