在堆上“分解” c++ 数组是不是安全?

Posted

技术标签:

【中文标题】在堆上“分解” c++ 数组是不是安全?【英文标题】:Is it safe to "dissolve" c++ arrays on the heap?在堆上“分解” c++ 数组是否安全? 【发布时间】:2021-03-27 01:14:18 【问题描述】:

我目前正在实现我自己的矢量容器,我遇到了一个非常有趣的问题(至少对我来说)。这可能是一个愚蠢的问题,但我不知道。

我的向量使用一个堆指针数组来堆分配未知类型的对象 (T**)。 我这样做是因为我希望单个元素的指针和引用保持不变,即使在调整大小后也是如此。

这会在构造和复制时以性能为代价,因为我需要在堆上创建数组以及在堆上创建数组的每个对象。 (堆分配比栈上慢,对吧?)

T** arr = new *T[size]nullptr;

然后对于每个元素

arr[i] = new Tdata;

现在我想知道如果不是单独分配每个对象,我可以在堆上创建第二个数组并将每个对象的指针保存在第一个数组中,这是否安全、有益(更快)和可能。然后使用(并删除)这些对象,就好像它们是单独分配的一样。

=> 在堆上分配数组是否比单独分配每个对象更快?

=> 在数组中分配对象并稍后忘记数组是否安全? (我觉得听起来很愚蠢)

链接到我的 github 仓库:https://github.com/LinuxGameGeek/personal/tree/main/c%2B%2B/vector

感谢您的帮助:)

【问题讨论】:

“未知类型 T”是什么意思?你的意思是模板类型参数吗? 您想要实现的是对类似双端队列的容器使用放置新分配。这是一个可行的优化,但通常这样做是为了减少分配调用和内存碎片,例如在某些 RT 或嵌入式系统上。在这种情况下,该数组甚至可能是一个静态数组。但是,如果您还要求 T 的实例占据相邻空间,这是一个自相矛盾的要求,使用它们会扼杀任何性能提升。 请注意,如果您使用placement new,则不应在创建的对象上使用delete,您必须直接调用析构函数。就delete 而言,放置新重载不是真正的new。您可能会或可能不会导致错误,但如果您使用静态数组肯定会导致崩溃,并且在删除与动态分配的数组开头具有相同地址的元素时会导致堆损坏。 std::vector + 内存池几乎是无与伦比的。只需使用它。 @nada 它没有为您提供对 OP 想要的元素的稳定引用。 【参考方案1】:

这会在构造和复制时以性能为代价,因为我需要在堆上创建数组,并且还要在堆上创建数组的每个对象。

复制POD 非常便宜。如果您研究完美转发,您可以实现构造函数和emplace_back() 函数的零成本抽象。复制时使用std::copy(),速度很快。

在堆上分配数组是否比单独分配每个对象更快?

每次分配都要求您向操作系统请求内存。除非您要求特别大量的内存,否则您可以假设每个请求将是一个恒定的时间量。与其要求 10 次停车位,不如要求 10 个停车位。

在数组中分配对象然后忘记数组是否安全? (我觉得听起来很愚蠢)

取决于您所说的安全。如果你不能自己回答这个问题,那么你必须清理内存并且在任何情况下都不能泄漏。

您可能会忽略清理内存的一个例子是,当您知道程序将要结束并且清理内存只是为了退出有点毫无意义时。不过,你应该清理它。阅读Serge Ballesta 答案,了解有关生命周期的更多信息。

【讨论】:

Each allocation requires you to ask the operating system for memory. 不太正确。堆功能更复杂,系统调用最小化。请注意,系统可以提供相当大的内存块,称为页面。 Each allocation requires you to ask the operating system for memory. new 后面的代码会花一些时间来减少系统调用的频率。【参考方案2】:

首先要说一句,你不应该从效率的角度考虑比较堆/堆栈,而是考虑对象的生命周期:

自动数组(你称之为堆栈)在定义它们的块的末尾结束它们的生命 动态数组(你为什么在堆上调用 )在被显式删除之前一直存在

现在,在一个数组中分配一堆对象总是比单独分配它们更有效。您保存了许多内部调用和各种数据结构来维护堆。简单地说,您只能释放数组而不是单个对象。

最后,除了一般可复制的对象,只有编译器而不是程序员知道确切的分配细节。例如(对于常见的实现)一个自动字符串(栈上)包含一个指向动态字符数组(堆上)的指针......

换句话说,除非您打算仅将容器用于 POD 或可复制的对象,否则不要期望自己处理所有分配和释放:非平凡对象具有内部分配。

【讨论】:

【参考方案3】:

堆分配比栈分配慢,对吧?

是的。动态分配是有代价的。

在堆上分配数组是否比单独分配每个对象更快?

是的。多次分配会使成本成倍增加。

我想知道这是否可能……如果不是单独分配每个对象,我可以在堆上创建第二个数组并将每个对象的指针保存在第一个数组中

这是可能的,但不是微不足道的。认真思考如何实现元素擦除。然后考虑如何使用包含已删除元素的索引的数组正确地实现其他功能,例如随机访问容器。

...安全

可以安全实施。

...有益(更快)

当然,将分配数从 N 减少到 1 本身就是有益的。但它是以某些方案为代价来实施擦除的。这个成本是否大于减少分配的好处取决于很多事情,例如容器的使用方式。

在数组中分配对象然后忘记数组是否安全?

“忘记”分配似乎是说“内存泄漏”的一种方式。


您可以使用自定义“池”分配器获得类似的优势。为您的容器实现对自定义分配器的支持可能更普遍有用。

附: Boost 已经有一个支持自定义分配器的“ptr_vector”容器。无需重新发明***。

【讨论】:

【参考方案4】:

我这样做是因为我想要指向个人的指针和引用 元素保持不变,即使在调整大小后也是如此。

您应该只使用std::vector::reserve 来防止矢量数据在调整大小时重新分配。

Vector 非常原始,但经过高度优化。你很难用你的代码打败它。只需检查其 API 并尝试其所有功能。要创建一些更高级的模板编程知识(显然您还没有)。

【讨论】:

请注意,这通常仅适用于在引用元素后没有添加、删除或重新排序的元素(除了从后面推/弹出)。 如果有人经常添加/删除不在矢量后面的项目,那么很可能应该使用其他东西而不是矢量。 也许这就是 OP 编写自己的容器的原因。 有可能,我已经发布了我的答案以纠正一些不正确的 OP 假设。其他事情看起来像是被其他答案所涵盖。 “原始”和“优化”并不矛盾,不需要“但是”。如果要使用“高级模板编程” - 结果可能不会像向量一样。对某些事情可能会更好,但对其他事情会更糟。【参考方案5】:

您想要提出的是使用placement new 分配用于类似双端队列的容器。这是一个可行的优化,但通常这样做是为了减少分配调用和内存碎片,例如在某些 RT 或嵌入式系统上。在这种情况下,该数组甚至可能是一个静态数组。但是,如果您还要求 T 的实例占据相邻空间,这是一个矛盾的要求,诉诸它们会扼杀任何性能提升。

...有益(更快)

取决于 T。对字符串或共享指针之类的东西没有必要这样做。或者任何实际上在其他地方分配资源的东西,除非 T 也允许改变这种行为。

我想知道如果不是分配每个 单独的对象,我可以在堆上创建第二个数组并 将每个对象的指针保存在第一个中

是的,即使使用标准 ISO 容器,这也是可能的,这要归功于分配器。 如果此“数组”似乎是多个写入者和读取者线程之间的共享资源,则存在线程安全或意识问题。您可能希望实现线程本地存储,而不是使用共享存储,并为交叉情况实现信号量。

通常的应用不是在堆上分配,而是在预先确定的静态分配的数组中分配。或者在程序开始时分配一次的数组中。

注意,如果你使用placement new,你不应该对创建的对象使用delete,你必须直接调用析构函数。就删除而言,放置新重载并不是真正的新重载。您可能会也可能不会导致错误,但如果您使用静态数组肯定会导致崩溃,并且在删除与动态分配的数组开头具有相同地址的元素时会导致堆损坏

【讨论】:

以上是关于在堆上“分解” c++ 数组是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

将 alloca() 用于可变长度数组是不是比在堆上使用向量更好?

堆上的 C++ 数组声明?

C++中,静态数组在内存中是存储在堆上,还是栈上,还是在静态存储区中?

C++:如何安全地释放堆分配的向量数组?

c ++ - 使用存储在堆上的数组填充对称矩阵

C++数组在内存中的分配