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

What are the usual im­ple­men­ta­tion de­tails be­hind mem­ory pools?

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

我只知道“内存池,也称为固定大小的块 allocation”per Wikipedia,我可以使用这些块来分配 我的物品的记忆。

内存池有标准规范吗?

我想知道这在堆上是如何工作的,怎么可能 已实施,应如何使用?

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

In case you haven't al­ready, fa­mil­iar­ize your­self with Boost.Pool. From the Boost doc­u­men­ta­tion:

What is Pool?

Pool al­lo­ca­tion is a mem­ory al­lo­ca­tion scheme that is very fast, but lim­ited in its us­age. For more in­for­ma­tion on pool al­lo­ca­tion (also called sim­ple seg­re­gated stor­age, see con­cepts con­cepts and Sim­ple Se­gre­gated Stor­age.

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

一个显示如何使用内存池的简单示例将被应用ci。

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

换句话说,不是调用 new/mallocdelete/free,而是调用您自定义的 allocator/deallocator函数。

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

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

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

第一种方法是定义自己的 Class 封装内存池并提供自定义方法来实现 newdelete 的语义,但从预分配池。请记住,这个池只不过是使用 new 分配的一块内存区域,并且具有任意大小。矿池版本 new/delete return resp。指点一下。最简单的版本可能看起来像 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)

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

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

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

所以,基本上,内存池为您提供了一个加速,您可以从潜在更复杂的应用程序代码的缺点中获得。不过话又说回来,有一些内存池的实现是经过验证的,可以简单地使用,比如boost::pool.

任何类型的“池”实际上只是您预先 acquired/initialized 的资源,以便它们已经准备好使用,而不是随每个客户请求即时分配。当客户端完成使用它们时,资源 returns 到池中而不是被销毁。

内存池基本上就是您预先分配的内存(通常是大块)。例如,您可能预先分配了 4 KB 的内存。当客户端请求 64 字节的内存时,您只需将一个指针交给他们,指向该内存池中未使用的 space,让他们可以随意读写。客户端完成后,您可以再次将那部分内存标记为未使用。

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

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();
}

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

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

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