【问题标题】:What are the usual im­ple­men­ta­tion de­tails be­hind mem­ory pools?内存池背后的常见实现细节是什么?
【发布时间】:2015-05-28 13:36:41
【问题描述】:

我试图了解使用内存池进行内存管理,但我 找不到太多关于它的信息,即使它似乎很常见 机制。

我所知道的只是“内存池,也称为固定大小的块 分配”根据维基百科,我可以使用这些块来分配 我的对象的内存。

有没有关于内存池的标准规范?

我想知道这在堆上是如何工作的,它是如何实现的 实现了,应该如何使用?

来自this ques­tion about C++11 mem­ory pool de­sign pat­terns,我读到:

如果您还没有熟悉 Boost.Pool,请先熟悉一下。 来自 Boost 文档:

什么是池?

池分配是内存分配 该方案非常快,但在使用方面受到限制。更多 池分配信息(也称为simple 隔离存储,参见con­cepts 概念和Sim­ple Se­gre­gated Stor­age。

我能理解他的意思,但这并不能帮助我理解如何 使用它们以及内存池如何帮助我的应用程序,如何实际 充分利用它们。

如果有一个简单的例子来说明如何使用内存池,我们将不胜感激。

【问题讨论】:

  • 看看boost::pool
  • 也许这有帮助:Memory Management
  • 感谢 boost::pool,@NathanOliver 我已经阅读了这个问题,但它没有显示内存池的基本概念,无论如何,谢谢
  • 基本前提是将分配的内存缓存或不返回给操作系统,如果你经常请求相同大小的内存,那么预先分配然后在使用时回收会更有效。客户端请求并返回内存,你可以认为这个一般原则适用于线程、句柄、套接字等基本上任何分配相对昂贵的资源

标签: c++ memory-management memory-pool


【解决方案1】:

任何类型的“池”实际上只是您提前获取/初始化的资源,以便它们已经准备好使用,而不是随每个客户请求动态分配。当客户端完成使用它们时,资源会返回到池中而不是被销毁。

内存池基本上只是您预先分配的内存(通常是大块)。例如,您可能会提前分配 4 KB 的内存。当客户端请求 64 字节的内存时,您只需将指针交给该内存池中未使用的空间,以便他们读取和写入所需的任何内容。客户端完成后,您可以再次将那部分内存标记为未使用。

作为一个基本示例,它不关心对齐、安全或将未使用(释放)的内存返回到池中:

class MemoryPool
{
public:
    MemoryPool(): ptr(mem) 
    {
    }

    void* allocate(int mem_size)
    {
        assert((ptr + mem_size) <= (mem + sizeof mem) && "Pool exhausted!");
        void* mem = ptr;
        ptr += mem_size;
        return mem;
    }

private:
    MemoryPool(const MemoryPool&);
    MemoryPool& operator=(const MemoryPool&);   
    char mem[4096];
    char* ptr;
};

...
{
    MemoryPool pool;

    // Allocate an instance of `Foo` into a chunk returned by the memory pool.
    Foo* foo = new(pool.allocate(sizeof(Foo))) Foo;
    ...
    // Invoke the dtor manually since we used placement new.
    foo->~Foo();
}

这实际上只是从堆栈中汇集内存。更高级的实现可能会将块链接在一起并进行一些分支以查看块是否已满以避免内存不足,处理作为联合的固定大小的块(空闲时列出节点,使用时为客户端提供内存),以及它肯定需要处理对齐(最简单的方法就是最大对齐内存块并为每个块添加填充以对齐后续块)。

更花哨的是伙伴分配器、slab、应用拟合算法的分配器等。实现分配器与数据结构没有太大区别,但你会深入原始位和字节,必须考虑对齐等问题,并且不能随机播放内容(不能使指向正在使用的内存的现有指针无效)。像数据结构一样,没有真正的黄金标准说“你应该这样做”。它们种类繁多,各有优缺点,但有一些特别流行的内存分配算法。

实际上,我会向许多 C 和 C++ 开发人员推荐实现分配器,只是为了更好地适应内存管理的工作方式。它可以让您更加了解所请求的内存如何连接到使用它们的数据结构,并且还可以在不使用任何新数据结构的情况下打开一扇全新的优化机会之门。它还可以使通常效率不高的链表等数据结构更加有用,并减少使不透明/抽象类型不那么不透明以避免堆开销的诱惑。然而,最初的兴奋可能会让你对所有的东西都硬塞自定义分配器,但后来才后悔额外的负担(特别是如果你在兴奋中忘记了线程安全和对齐等问题)。值得在那里放松一下。与任何微优化一样,它通常最好在事后独立应用,并且手头有分析器。

【讨论】:

  • 谢谢,这正是我要找的答案!!
【解决方案2】:

内存池的基本概念是为您的应用程序分配大部分内存,然后,您将返回之前的一大块内存,而不是使用普通的new 从 O/S 请求内存而是分配内存。

为了做到这一点,你需要自己管理内存使用,不能依赖O/S;即,您需要实现自己的 new 和 delete 版本,并且仅在分配、释放或可能调整自己的内存池时使用原​​始版本。

第一种方法是定义自己的 Class 封装内存池并提供自定义方法来实现 new 和 delete 的语义,但从预分配的池中获取内存。请记住,这个池只不过是一个使用new 分配的内存区域,并且具有任意大小。池的new/delete 版本分别返回。采取指针。最简单的版本可能看起来像 C 代码:

void *MyPool::malloc(const size_t &size)
void MyPool::free(void *ptr)

您可以使用模板来自动添加转换,例如

template <typename T>
T *MyClass::malloc();

template <typename T>
void MyClass::free(T *ptr);

请注意,由于模板参数,size_t size 参数可以省略,因为编译器允许您在 malloc() 中调用 sizeof(T)。

返回一个简单的指针意味着您的池只能在有相邻内存可用时增长,并且只有在其“边界”处的池内存未被占用时才会缩小。更具体地说,您不能重新定位池,因为这会使 malloc 函数返回的所有指针无效。

解决此限制的一种方法是返回指向指针的指针,即返回 T** 而不是简单的 T*。这允许您在面向用户的部分保持不变的情况下更改底层指针。顺便说一句,NeXT O/S 已经这样做了,它被称为“句柄”。要访问句柄的内容,必须调用(*handle)-&gt;method() 或(**handle).method()。最终,Maf Vosburg 发明了一个伪运算符,利用运算符优先级摆脱(*handle)-&gt;method() 语法:handle[0]-&gt;method();,它被称为sprong operator。

此操作的好处是:首先,您避免了对 new 和 delete 的典型调用的开销,其次,您的内存池确保您的应用程序使用连续的内存段,即它避免了内存碎片,因此增加了 CPU 缓存命中率。

因此,基本上,内存池可以为您提供加速,但可能会带来更复杂的应用程序代码的缺点。但话又说回来,有一些内存池的实现已经过验证并且可以简单地使用,例如boost::pool。

【讨论】:

  • 我不认为更复杂的代码真的是主要缺点。如果处理得当,分配器模块应该或多或少是自包含的(正如 boost::pool 所证明的那样)。我认为主要的缺点是实现内存池所需的内存使用损失。
  • 你有一些简单的内存池实现代码吗?
  • 我没有测试过,但这看起来很合理:github.com/cacay/MemoryPool/blob/master/C-11/MemoryPool.tcc
  • @Technaton 感谢您的链接
  • 来自不重要的纠正部门:Maf Vosburg 将 handle[0]-&gt;method 技巧命名为 "sprong" 不是“sproing”。他的页面(从 1998 年开始)遭受了比特腐烂,唉,我只能找到 Wayback Machine 参考。
【解决方案3】:

基本上,内存池允许您避免在频繁分配和释放内存的程序中分配内存的一些费用。您所做的是在执行开始时分配一大块内存,并将相同的内存用于不重叠的不同分配。您必须有一些机制来跟踪可用内存并使用该内存进行分配。用完内存后,不要释放它,而是将其再次标记为可用。

换句话说,不要调用new/malloc 和delete/free,而是调用您自定义的分配器/释放器函数。

这样做可以让您在执行过程中只进行一次分配(假设您大约知道总共需要多少内存)。如果您的程序受延迟而不是内存限制,您可以编写一个执行速度比malloc 更快的分配函数,但会消耗一些内存。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-03
    • 1970-01-01
    • 2011-06-22
    • 2012-06-13
    • 2016-09-19
    • 2010-11-06
    相关资源
    最近更新 更多