如何在不使指向它的指针失效的情况下增长缓冲区



术语" pool "one_answers" buffer "在这里可以互换使用
假设我有一个池,我想在程序的开始分配,因为不总是调用new
现在,我不想人为地限制池的大小,但是如果我重新分配一个更大的池,所有指向旧池的指针都将失效,这当然不是很酷。


我想到的一种方法是"分页",也就是
const int NUM_PAGES = 5;
char* pool[NUM_PAGES];

分配一个新页面,而不是只重新分配一个页面。这将使所有指针保持有效,但使分页池的管理变得更加困难。另外,我限制了自己的页面数量,所以最后又限制了池的大小。


另一种方法是从分配函数返回的指针映射到指向实际内存空间的指针。这将使所有旧指针保持有效,但会占用更多内存,并且我需要编写一个智能指针从分配函数返回,该函数执行映射。

还有哪些其他的方法可以达到我想要的?在上面的示例实现中,我错过了哪些优点?

你说的东西让我想起了std::deque。我不确定您是否可以原样使用std::deque,或者您是否只需要使用其基本设计来实现某种分配器。

扩展页面"池"的概念,如果您将页面存储为链表呢?

要从池中分配新数据,您应该只需要访问顶部"页",它将位于列表的头部,所以这是O(1)。如果您需要增加池的总大小,则分配一个新页面并将其推入列表的头部,也是O(1)。

我基本上对池分配程序使用相同的想法,但也使用最近被释放的项的"空闲列表"…

编辑:根据你的评论,如果你也想利用释放的数据,你也可以存储一个空闲列表,也可能是一个链表。因此,当您释放数据时,将指针和大小标记压入空闲列表。从池中分配数据时,首先检查空闲列表中是否有可用项,否则从池中分配。

标准内存管理器通常已经这样做了,所以这种方法并不总是更好。具体来说,我通常只在分配的项大小相同时才使用这种类型的自定义分配器(这样遍历空闲列表的时间为O(1)!)。std::list的自定义分配器就是一个例子。

希望这对你有帮助。

考虑使用boost池

当然,有一个问题是,为什么要这么麻烦自己呢?

你说你希望避免new开销,但是为什么不选择一个更好的new实现呢?例如,tcmallocjemalloc通常是多线程应用程序的很好的竞争者。

你想创建的是非常相似的,事实上,写一个自定义的malloc/new实现。因此,如果您真的不想使用经过验证的实现,那么您将受益于那些已经使用过的人的见解。

我个人的兴趣倾向于BiBOP策略(Big Bag of Pages)来对抗碎片化。其思想是为每个大小的分配分配一个专用池,因此一个简单的空闲列表(与分配交错)就足够了(不需要合并)。通常,只要请求的大小小于页面大小(我见过使用4KB),并且任何更大的大小都是单独分配的(在几个页面中),就会这样做。丢弃的页面被回收。

我的主要兴趣是BiBOP很容易维护一个间隔树地址范围>页头,从而从(可能的)内部元素(如属性)的地址确定对象的完整大小,这对于垃圾收集(参考下面的步骤)很有用。

对于多线程分配,tcmallocjemalloc使用两种不同的方法:

  • jemalloc使用每线程池:固定数量的线程/线程池很好,但使创建线程的过程成本更高
  • tcmalloc使用一个全局多线程池,使用无锁技术,并尝试在可用池上的线程进行负载平衡,以限制争用,如果线程使用的池被"锁定"(而不是等待),则让线程寻找新的池,并让每个线程在线程局部变量中缓存最后使用的池。因此,线程是轻量级的,但是如果池的数量低于活动线程的数量,可能会出现一些争用。

几点思考:

  • 当你有std::vector<T*>容器时,添加元素并触发resize会使该容器中的引用/指针/迭代器失效,但不会使直接指向对象的引用/指针失效。因此,一个间接层可能会解决你的问题,这取决于你真正想用这些引用/指针/迭代器做什么。

  • 在具有虚拟内存和大地址空间的系统中,您可以进行大量分配,而无需从物理内存资源中实际分配页面,直到它们被写入。因此,在这样的系统上,您可以为vector初始设置比需要的更大的容量,而不会觉得浪费了任何有价值的东西。

  • 其他容器——std::map<>std::list<>——在添加新元素时不会移动它们现有的元素,因此迭代器/指针/引用仍然有效。

最新更新