【问题标题】:Does an unused STL container allocate memory?未使用的 STL 容器是否分配内存?
【发布时间】:2016-06-07 03:45:34
【问题描述】:

给定代码:

class Foo {
  std::vector<int> items;
  std::map<int, int> dictionary;
};
  1. 如果上述向量或映射中没有添加任何内容,是否仍会分配一块缓冲内存? (换句话说,缓冲区分配是否总是在容器创建期间发生,还是可以推迟到调用 push_back 之类的函数?)

  2. 是否有处理初始 STL 容器缓冲区分配时间的标准,或者该行为是否允许在 STL 容器和编译器之间有所不同?

注意:这个问题不是关于这样的容器会添加到类 Foo 的大小的额外字节。

(这个问题的一个相关子集强调分配大小是Initial capacity of vector in C++。)

【问题讨论】:

  • 我怀疑标准是否定义了这样的东西。您可以通过使用自定义分配器制作容器来检查您的实现。
  • 标准并没有说什么,但大多数实现都会为vector做一些预分配,并且不会为map预分配任何东西。
  • @DOUGLASO.MOEN 是对象的大小,而不是内部缓冲区的大小。
  • 如果实现实际上在初始化时分配了任何内存,我会感到惊讶,因为我看不出将时间浪费在没有添加任何内容的潜在代码路径上符合人体工程学。我认为这样做没有任何好处,但有明显的缺点。我所看到的是实现分配而不是在分配时立即需要。
  • @DavidHaim:没有什么能阻止容量随着0, 8, 16, 32, 64 增长(跳过第一个小值)。没有预分配允许保留正确的大小,只有 1 个分配而不是 2 个。容量增长的变化只增加了一个分配测试。

标签: c++ stl stdvector allocation stdmap


【解决方案1】:

C++ Reference 在 C++17 中,默认构造函数是 noexcept 如果分配器构造是 noexcept。所以这取决于使用的分配器。在 VS 2015 中,标准构造函数是 noexcept

澄清:这意味着如果分配器不是noexcept,则不会分配内存块。

对于你的第二个问题:相同的参考,它是 O(1)。

【讨论】:

  • 跟题有什么关系?
  • 如果分配器默认分配一块内存,那么分配器的构造函数不能是noexcept,因为内存分配可能会因异常而失败。
  • 表示如果分配器不是noexcept,则不分配内存块。这是第一个问题。
  • @SergeyA - 实际上他说得有道理。如果默认构造函数是noexcept,并且allocator::allocate 抛出 - 这意味着容器在构造时无法请求内存。因此向量/映射不能预分配内存,因为 allcoation 可以抛出,违反 noexcept
  • 当我重新考虑这一点时,noexcept 与此无关。向量可以 try 分配内存并从分配器中捕获任何异常,然后传递预分配。具有 noexcept 构造函数对 preallcoation 没有任何说明,因为向量至少可以尝试并通过,而不会传播分配器异常
【解决方案2】:

标准没有说明任何内容,但我专门研究的实现将为std::vector 进行一些预分配,并且不会为std::map 预分配任何内容。

这实际上曾经给我带来了很大的打击,当我拥有一个巨大的容器时,其中的元素有一个很小的 ​​- 不超过 10 个元素,大多数条目都有 0 大小的向量 - 向量在其中。此实现中的默认向量容量为 32,而 '32 * sizeof(vector_element) * number_of_elements' 恰好非常大。

【讨论】:

  • 为什么“线性化常量 push_back 复杂度”需要预分配?
  • FWIW,例如,GCC 4.8.4 和 4.9.2 中的 std::vector&lt;int&gt;capacity() 为 0 开始,然后增长为 1,2,4,8,16, ……;与 clang-3.5 相同。
  • @DieterLücking,试图计算复杂性,发现它太无聊并决定完全删除该语句。
  • 这是哪个实现?老实说,这听起来像是非常愚蠢的实现行为。
  • 初始容量大于零的std::vector 似乎不太可能代表大多数实现。即使标准允许,我当然会认为任何这样的实现都是错误的。我可以更宽容一些其他容器(例如,std::unordered_map),但向量和字符串不应该做不必要的动态分配。
【解决方案3】:

如前所述,这没有很好的定义。但是,您可以对其进行测试。

例如使用 gcc/linux。制作一个简单的程序,用-O0 -g 编译它并在gdb 中运行它。那么

break main
run
break malloc
cont

现在只需在每个 malloc 上运行 backtrace,您就会看到动态分配。在我的 gcc 5.3.0 中,两个空容器都不分配堆内存,这是在第一个 push_back / operator[] 上完成的。

当然,如果不是 gdb / malloc,您应该使用您喜欢的调试器并中断分配器的底层函数。

现在,如果您考虑这两种情况。在这种情况下预分配内存是否有意义?

std::vector<int> foo;
foo.push_back(13);

嗯,从技术上讲,您可以保存对 nullptr 的检查,但使用将向量实现为 3 个指针的常用方法,无需进行额外检查。

但考虑

std::vector<int> foo;
foo.reserve(100);

在这种情况下,预分配会损害性能。

我找不到为树结构(例如 map)预分配的论据。

请记住,这是一个非常具体的优化。仅在有充分理由的情况下对此进行优化(基准!)。

注意:您可能想了解小字符串优化,这是一种非常常见的技术,相关但不同。

【讨论】:

  • malloc 上设置断点假定分配器使用malloc。不难想象分配器依赖于较低级别的东西来分配动态内存。更好的测试方法是替换您自己的分配器,然后查看容器是否调用它。
  • 我确实提到它可能不是malloc,当然使用你自己的分配器进行测试会更通用。但这是关于测试一个特定的实现——务实并使用已经存在的东西似乎是最好的方法。还要考虑vector 可以为默认分配器和您的自定义分配器以不同方式实现。
【解决方案4】:
  1. 如果上述向量或映射中没有添加任何内容,是否仍会为潜在条目分配一块内存? (换句话说,条目分配是否总是在容器创建期间发生,还是可以推迟到调用 push_back 之类的函数时进行?)

这可能发生,是的。它实际上是容器实现的一个细节,标准中没有规定。

  1. 是否有处理初始 STL 容器分配时间的标准,或者该行为是否允许在 STL 容器和编译器之间有所不同?

您可以使用例如延迟创建std::unique_ptr 用于成员,并通过调用 getter 函数来创建它们。

【讨论】:

  • @knivil 的回答指出它在更新的标准工作中间接指定的,受到其他功能的约束。
【解决方案5】:

值得注意的是,微软的 STL 实现目前确实std::mapstd::set 的默认构造函数中分配。 Godbolt example - 请注意在程序集输出的第 8 行对 operator new 的调用。

是的,这绝对会影响性能。我是在对一些神秘的慢代码进行分析后才意识到这一点的,当时发现带有很少使用的map 成员的类的默认构造函数主导了相关循环的运行时。

如果您像我一样对这个实施决定不满意,我会指出Boost.Container 的对应物do guarantee 零分配默认构造。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-08-10
    • 2011-12-02
    • 1970-01-01
    • 1970-01-01
    • 2011-05-08
    • 1970-01-01
    • 1970-01-01
    • 2015-12-15
    相关资源
    最近更新 更多