【问题标题】:What the heque is going on with the memory overhead of std::deque?std::deque 的内存开销是怎么回事?
【发布时间】:2011-05-04 14:11:43
【问题描述】:

我正在研究一种使用std::queue 的外部排序算法,并且必须仔细限制其内存使用量。我注意到在合并阶段(使用几个固定长度的std::queues),我的内存使用量增加到我预期的大约 2.5 倍。由于std::queue 默认使用std::deque 作为其底层容器,我在std::deque 上运行了一些测试以确定它的内存开销。以下是在 VC++ 9 上运行的结果,在发布模式下,使用 64 位进程:

将 100,000,000 chars 添加到 std::deque 时,内存使用量将增长到 252,216K。注意 100M chars(1 字节)应该占用 97,656K,所以这是 154,560K 的开销。

我用doubles(8 字节)重复测试,发现内存增长到 1,976,676K,而 100M doubles 应该占用 781,250K,开销为 1,195,426K!!

现在我了解到std::deque 通常实现为“块”的链表。如果这是真的,那么为什么开销与元素大小成正比(因为指针大小当然应该固定为 8 个字节)?为什么它这么大?

任何人都可以解释为什么std::deque 使用这么多危险的内存吗?我想我应该将我的std::queue 底层容器切换到std::vector,因为没有开销(假设适当的大小是reserveed)。我认为std::deque 的好处在很大程度上被这样一个事实所抵消,因为它具有如此巨大的开销(导致缓存未命中、页面错误等),并且复制std::vector 元素的成本可能会更少,鉴于整体内存使用量要低得多。这只是微软对std::deque 的错误实现吗?

【问题讨论】:

  • 第一个问题。您如何确定内存使用情况。因为有些方法不如其他方法准确。
  • @Martin,我只是观察任务管理器中进程的“峰值工作集”值。
  • 如果你编写一个程序来分配 2M 的数据(以块的形式)然后释放它,然后在退出之前等待用户输入它是否表现出相同的行为。即内存上升然后进入稳定状态但不会下降。 PS> 找不到“Peak Working Set”
  • 我想我对 Windows 的虚拟内存系统有足够的了解,可以说当内存被释放时(C++ 中的deleted),它确实会返回给操作系统。在 Windows 中,内存可以“保留”和“提交”。 “峰值工作集”显示了进程在其生命周期内使用的最大物理 RAM 量,当数量不足以需要交换文件时,它等于“提交的”虚拟内存。我已经确认deleteing 内存会导致内存被释放到操作系统,并且工作集大小相应减小。在我的简单测试中,...
  • ...没有释放内存。内存使用量在循环中稳步增加。与std::vector 不同,它确实会分配一个比现有块更大 的新连续块(这反映在“峰值工作集”和“工作集”之间的差异上),复制元素,然后释放原始文件,std::deque 不会发生这种情况。它以 16 字节块的形式增长(由以下帖子确定)并且不会在插入时释放内存。

标签: c++ visual-c++ memory stl


【解决方案1】:

您是否可能正在运行调试二进制文件? 100M 个字符的 252MB 看起来确实很多......

您可以使用umdh 来检查此属性的归属,以在之前和之后进行快照,然后比较两者 - 可能会阐明为什么它比您预期的要大。

编辑: 仅供参考 - 当我在 VS2010 的调试器之外运行它时,我得到 181MB 和 chars。

deque<char> mydequeue;
for (size_t i = 0; i < 100 * 1024 * 1024; ++i)
{
  mydequeue.push_back(char(i));
}

编辑:支持@Dialecticus 的另一个答案,这给了我与double 相同的足迹:

struct twoInt64s
{
public:
    twoInt64s(__int64 _a, __int64 _b) : a(_a), b(_b) {}

    __int64 a;
    __int64 b;
};

编辑:修改 _DEQUESIZ 后(每块 128 个字符),现在 100M 字符占用 113M 内存。

我的结论是,您看到的剩余开销是由于 deque 块的管理结构,它有 16 个字符的数据,加上 deque 的控制信息加上堆管理器的更多控制信息。

#define _DEQUESIZ   (sizeof (value_type) <= 1 ? 128 \
    : sizeof (value_type) <= 2 ? 8 \
    : sizeof (value_type) <= 4 ? 4 \
    : sizeof (value_type) <= 8 ? 2 \
    : 1)    /* elements per block (a power of 2) */

道德 - 如果您真的想为您的特殊目的优化它,请准备好使用&lt;deque&gt;。它的行为主要取决于元素的大小,并且超出了预期的使用模式。

编辑:根据您对队列大小的了解,您可能可以使用boost::circular_buffer 作为 std::queue 容器的替代品。我敢打赌,这会表现得更像你想要的(和预期的)。

【讨论】:

  • 不,这是不可能的。我正在以发布模式(使用 /O2 优化)构建我的测试应用程序并链接到发布运行时库。在调试模式下,内存使用率要高得多:100M 双打情况下为 4,327,684K
  • @Tabber33 - 查看编辑,在 IDE 之外运行测量以避免低效的堆选项
  • 刚刚在 IDE 外运行 -- 内存使用情况相同。
  • @Tabber33 - 多么奇怪。 FWIW 我得到 754 MB 和 25M doubles。这是来自 Process Explorer 的私有字节。
  • 您也可以使用环境变量_NO_DEBUG_HEAP=1 运行,即使在IDE 中也可以禁用调试堆。
【解决方案2】:

查看 _DEQUESIZ(每个块的元素数)的代码:

#define _DEQUESIZ   (sizeof (_Ty) <= 1 ? 16 \
    : sizeof (_Ty) <= 2 ? 8 \
    : sizeof (_Ty) <= 4 ? 4 \
    : sizeof (_Ty) <= 8 ? 2 : 1)    /* elements per block (a power of 2) */

如果元素更大,它会变得更小。只有大于 8 字节的元素才会获得预期的行为(开销随着元素大小的增加而减少百分比)。

【讨论】:

  • 这是微软的代码吗?为什么他们会尝试使用只有 16 个字节的块?太疯狂了!它肯定会解释为什么开销非常糟糕!典型的内存管理器每个分配的块有 8-16 字节的开销;调试版本和 64 位进程更糟糕。此外,如此多的分配(和解除分配)可能会很慢。
  • 是Dinkumware的代码——微软C++标准库的实现。
  • @Qwertie:我也喜欢这样一个事实,即他们更喜欢一遍又一遍地计算事物并依赖编译器来优化计算,而不是使用 static size_t const 的成员 static size_t const。它肯定会,但这似乎仍然是对宏的滥用......
  • 另外,我忘了提到存储指向每个块的指针所需的 4+ 字节开销。顺便说一句,当用户分配大量相同大小的小块时,内存管理器可以实现较小的开销(0-4 字节)......我知道是因为我专门为此目的编写了一个内存管理器。但我不认为微软提供了一个高效的内存管理器,而且我听说调试版本每个块有 36 个字节的开销,并且总大小四舍五入到接下来的 16 个字节;见nobugs.org/developer/win32/debug_crt_heap.html
  • “低碎片堆”如何影响每个分配的字节开销?
【解决方案3】:

不看你正在使用的 std::queue 的实际实现,我的猜测是它的内存分配看起来像这样:

if (new element won't fit) {
    double the size of the backing storage
    realloc the buffer (which will probably copy all elements)
}

加倍而不是更保守的原因是您希望queue.push_pack 操作的平均时间为 O(1)。由于重新分配可能会复制现有元素,因此当您最初将所有值推入队列时,仅根据需要增长数组(一次 1 个元素)的版本将是 O(n^2)。我将把它作为练习留给读者,加倍版本如何给出恒定的平均时间。

由于您引用了整个过程的大小,因此当您推动略高于 2 的幂 (2^26

【讨论】:

  • queuedeque 的适配器,默认情况下。 deque 不是这样工作的——它以块的形式分配内存并保持它们的链来代表整个容器。 vector 更有可能执行您在此处描述的操作。
  • reserve 未被调用时,您所描述的对于std::vector 来说有些准确,但对于std::deque 则完全不准确。
猜你喜欢
  • 2011-09-24
  • 2010-09-29
  • 1970-01-01
  • 1970-01-01
  • 2020-11-29
  • 2015-08-27
  • 2011-07-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多