在 C++ 中使用 realloc

Posted

技术标签:

【中文标题】在 C++ 中使用 realloc【英文标题】:Using realloc in c++ 【发布时间】:2011-05-04 14:28:48 【问题描述】:

std::realloc 在 c++ 中很危险,如果 malloc 的内存包含非 pod 类型。似乎唯一的问题是std::realloc 如果不能原地增加内存,就不会调用类型析构函数。

一个简单的解决方法是使用try_realloc 函数。如果它不能就地增长,它不会分配新内存,而是简单地返回 false。在这种情况下,可以分配新内存,将对象复制(或移动)到新内存,最后释放旧内存。

这似乎非常有用。 std::vector 可以充分利用这一点,可能避免所有复制/重新分配。抢先式阻燃:从技术上讲,这与 Big-O 性能相同,但如果向量增长是一个瓶颈即使 Big-O 保持不变,您的应用程序的 2 倍加速也很好。

但是,我找不到任何像 try_realloc 一样工作的 c api。

我错过了什么吗? try_realloc 没有我想象的那么有用吗?是否有一些隐藏的错误导致try_realloc 无法使用?

更好的是,是否有一些记录较少的 API 可以像 try_realloc 一样执行?

注意: 显然,我在这里使用库/平台特定代码。我不担心try_realloc 本身就是一种优化。


更新: 在 Steve Jessops 评论vector 是否会更有效地使用 realloc 之后,我写了一个概念证明来测试。 realloc-vector 模拟向量的增长模式,但可以选择重新分配。我在向量中运行了多达一百万个元素的程序。

作为比较,vector 必须分配 19 次,同时增长到一百万个元素。

结果,如果 realloc-vector 是唯一使用堆的东西,那么结果非常棒,3-4 分配,同时增长到百万字节的大小。

如果realloc-vectorvector 一起使用,vector 的增长速度是realloc-vector 的 66%,结果不太乐观,在增长过程中分配了 8-10 倍。

最后,如果realloc-vector 与以相同速率增长的vector 一起使用,则realloc-vector 分配17-18 次。几乎没有比标准向量行为节省一个分配。

我不怀疑黑客可以通过游戏分配大小来节省成本,但我同意 Steve 的观点,即编写和维护这样一个分配器的巨大努力并没有带来任何好处。

【问题讨论】:

在不知道您要定位的平台的情况下,很难提供特定于平台的建议。 我忍不住想:如果你想要最好的性能,使用 vector.reserve() 这样你就根本不用增长向量了。 @kotlinski:但你不能总是那样做。否则向量类的动态增长属性无论如何都是多余的。 如果您的vector 持有的对象的复制性能很差,并且无论出于何种原因您都不能使用deque,那么也许您应该将您的vector 更改为持有@987654343 @ 指向对象的实例。这样,复制操作将变得便宜很多。我不确定unique_ptr 对象是否可以在标准容器中使用,但这会进一步减少复制开销。 为了能够在 C++ 中将 realloc(或类似的)与非 POD(普通旧数据)一起使用,您不仅需要能够在失败的情况下调用析构函数,而且在这种情况下缩小数组。如果数组正在增长,它还必须在数组的新成员上调用默认构造函数。可能需要考虑的另一件事是移动对象是否会导致某些问题;然后类可能需要实现一个 move 方法,该方法是一种析构函数,它具有对旧数据和新数据的引用,但移动的顺序可能很重要。 【参考方案1】:

vector 通常会以较大的增量增长。你不能在不重新定位的情况下重复这样做,除非你仔细安排事情,以便在向量的内部缓冲区上方有大量空闲地址(这实际上需要分配整个页面,因为显然你不能有其他分配稍后在同一页面上)。

所以我认为,为了在这里获得真正好的优化,您需要的不仅仅是一个“简单的解决方法”,如果可能的话,它会进行廉价的重新分配 - 您必须以某种方式为制作做一些准备有可能,并且准备工作会花费您的地址空间。如果您只对某些向量进行此操作,即那些表明它们会变大的向量,那么这将毫无意义,因为它们可以用reserve() 表示它们会变大。如果您有一个巨大的地址空间,您只能对所有向量自动执行此操作,这样您就可以在每个向量上“浪费”一大块。

据我了解,Allocator 概念没有重新分配功能的原因是为了保持简单。如果std::allocator 有一个try_realloc 函数,那么要么每个分配器都必须有一个(在大多数情况下无法实现,并且只需要总是返回false),否则每个标准容器都必须是专门为std::allocator 使用它。这两个选项都不是一个很好的 Allocator 接口,尽管我认为对于几乎所有 Allocator 类的实现者来说,仅仅添加一个无操作的 try_realloc 函数并不是一项巨大的工作。

如果vector 因重新分配而变慢,deque 可能是一个不错的替代品。

【讨论】:

我假设try_realloc 非常便宜。当超过vector 的容量而不是默认的指数增长时,它将使用try_realloc 将容量增加一。当try_realloc 在没有重新分配的情况下无法再增长时,分配两倍所需的内存,执行标准的复制/移动操作,使容量翻倍。 一般我不明白为什么 try_realloc 应该比 malloc 快得多。 @caspin:这有什么比vector 实际做的更有效,但是(为了争论)每次重新定位时它的容量仍然翻倍?我认为,您所获得的只是在您的版本中,额外的内存还没有“真正分配”,因此可以分配给其他用途。如果它没有被如此使用,那么就没有区别,如果它被如此使用,那么你实际上已经 减慢 向量,因为你的 try_realloc 将在我的额外容量被使用之前失败向上。即使在我的版本中,通过过度使用,您也可以避免“真正使用”物理内存。 @caspin:我认为你的方式会在这种情况下获得收益,纯粹是靠运气,当向量想要扩展到缓冲区时,缓冲区上方有大量空闲内存。这很好,但正如我所说,我认为在界面中构建它太罕见了,因为deque 的存在是为了支持增长不可预测且复制性能很糟糕的情况。也许标准的作者也有同样的想法,或者他们有其他原因——我不知道。 @caspin:公平点。如果vector 愿意做出特定于平台的假设/猜测,那么理论上它可以确保其增加的容量序列完全适合内存分配器实际提供的块大小,因此这种向量性能的坏情况永远不会发生.该序列可能是 32、64、128,或者可能是 24、56、120。如果您想将这部分作为可移植分配器接口的一部分,我认为您将付出很多努力来避免使用 @987654338 @ ;-)【参考方案2】:

您可以实现类似于您建议的try_realloc 的东西,将mmapMAP_ANONYMOUSMAP_FIXEDmremapMREMAP_FIXED 一起使用。

编辑:刚刚注意到 mremap 的手册页甚至说:

mremap() 使用 Linux 页表 方案。 mremap() 改变 之间的映射 虚拟地址和内存页。这可以用来实现 一个非常有效的 重新分配(3)。

【讨论】:

问题在于mmap 的最小分配是一个页面,这对于大多数向量来说是巨大的过度杀伤。 同意,但我认为没有任何子页面大小的分配器可以提供此功能,如果它明显小于页面大小,那么性能优势可能无论如何都可以忽略不计。 【参考方案3】:

C 中的realloc 只不过是一个便利函数;它对性能/减少副本几乎没有好处。我能想到的主要例外是分配一个大数组的代码,然后在知道所需大小后减小大小 - 但即使这样也可能需要在某些 malloc 实现上移动数据(那些严格按大小分隔块的实现)所以我考虑realloc 的这种用法真的很糟糕。

只要不是每次添加元素时都不断地重新分配数组,而是在空间用完时以指数方式增长数组(例如 25%、50% 或 100%),只需手动分配新内存、复制和释放旧内存将产生与使用realloc 大致相同(并且在内存碎片的情况下相同)的性能。这肯定是 C++ STL 实现使用的方法,所以我认为您的全部担忧是没有根据的。

编辑realloc 真正有用的一个(罕见但并非闻所未闻的)情况是用于具有虚拟内存的系统上的巨型块,其中 C 库与内核交互以重定位整个页面到新地址。我说这很少见的原因是因为您需要处理非常大的块(至少几百 kB),然后大多数实现甚至会进入处理页面粒度分配的领域,并且可能更大(可能有几个 MB)在进入和退出内核空间之前重新排列虚拟内存比简单地复制要便宜。当然,try_realloc 在这里没有用处,因为所有的好处都来自于实际采取行动成本低廉。

【讨论】:

我认为一旦分配增长到兆字节,指数分配策略的绝对开销可能会开始成为问题,因此仅在该大小范围内进行优化仍然是好的。 realloc 好的另一种情况是,当有一个循环只重新分配一个内存块时,它最终应该在堆位置结束,在那里它可以在不复制的情况下增长。并且复制的绝对成本也随着块大小的增加而增加,因此仅适用于大块的优化仍然是好的。

以上是关于在 C++ 中使用 realloc的主要内容,如果未能解决你的问题,请参考以下文章

C++ 如何在 C++ 中使用 dlopen()?

如何在 Visual C++ 2010 中使用 C++ 库 [重复]

我们啥时候需要在纯 C++ 程序中使用结构?纯 C++ 程序是不是需要结构? [复制]

当python使用“Python.h”调用该c++进程时,如何在python中停止一个c++进程

在 Objective-C 中使用外部 C++ 头文件

使用多个视图在 QML 中查看、编辑和更新数据(来自 C++),而数据保留在 C++ 中(订阅数据)