内存池背后的常见实现细节是什么?

时间:2015-05-28 13:36:42

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

我试图理解使用内存池进行内存管理,但是我 虽然它看起来很普遍,但是找不到它 机构。

我所知道的就是“内存池,也称为固定大小的块 分配“每个维基百科,我可以使用这些块来分配 我的物品的记忆。

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

我想知道它是如何在堆上工作的,它是如何工作的 实施,以及如何使用它?

this ques­tion about C++11 mem­ory pool de­sign pat­terns开始,我读过:

  

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

     
    

什么是游泳池?

         

池分配是内存分配     方案非常快,但使用受限。更多     有关池分配的信息(也称为简单     隔离存储,请参阅con­cepts个概念和Sim­ple Se­gre­gated Stor­age

  

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

显示如何使用内存池的简单示例。

3 个答案:

答案 0 :(得分:23)

任何类型的游泳池"实际上只是您提前获得/初始化的资源,以便他们已经准备就绪,而不是随着每个客户请求即时分配。当客户端完成使用它们时,资源将返回池而不是被销毁。

内存池基本上只是您提前分配的内存(通常是大块)。例如,您可以提前分配4千字节的内存。当客户端请求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();
}

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

更好看的是伙伴分配器,平板,应用拟合算法的等等。实现分配器与数据结构没有太大区别,但是你得到了原始位和字节的深度,必须考虑像对齐这样的东西,并且不能对内容进行随机播放(无法使现有的指向正在使用的内存的指针无效)。就像数据结构一样,并没有真正的黄金标准说,&#34;你应该这样做&#34;。它们的种类繁多,各有各的优缺点,但有一些特别流行的内存分配算法。

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

答案 1 :(得分:6)

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

为了使这项工作,你需要自己管理内存使用,不能依赖于操作系统;即,您需要实现自己的newdelete版本,并且仅在分配,释放或可能调整自己的内存池大小时才使用原始版本。

第一种方法是定义一个自己的类,它封装了一个内存池,并提供了实现newdelete语义的自定义方法,但是从前面获取内存分配池。请记住,此池只是使用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参数可以省略,因为编译器允许您在sizeof(T)中调用malloc()

返回一个简单的指针意味着你的池只有在相邻的内存可用时才会增长,并且只有当池中的内存位于其边界时才会缩小。没有被采取。更具体地说,您无法重新定位池,因为这会使malloc函数返回的所有指针无效。

修复此限制的一种方法是返回指针指针,即返回T**而不是简单地T*。这允许您在面向用户的部分保持不变时更改基础指针。在内部,已经为NeXT O / S做了,它被称为&#34;句柄&#34;。要访问句柄的内容,必须致电(*handle)->method()(**handle).method()。最终,Maf Vosburg发明了一个伪运算符,利用运算符优先级来摆脱(*handle)->method()语法:handle[0]->method();它被称为sprong operator

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

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

答案 2 :(得分:1)

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

换句话说,不是调用new / mallocdelete / free,而是调用自定义的分配器/解除分配器函数。

这样做只允许您在执行过程中进行一次分配(假设您大致知道您需要多少内存)。如果你的程序是延迟而不是内存限制,你可以编写一个比malloc执行速度快的分配函数,代价是一些内存使用。