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

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

本文关键字:细节 是什么 实现 常见 背后 内存      更新时间:2023-10-16

我正试图取消对内存池的管理,但我找不到太多关于它的信息,尽管它看起来很常见机械主义。

我所知道的是,"记忆池,也称为固定大小的块al­lo­ation">每个Wiki百科全书,我可以使用这些块来记住我的目标。

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

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

从这个关于C++11内存池设计模式的问题中,我读到了:

如果你还没有准备好,可以用Boost.Pool来放大你的自我。来自Boost文档:

什么是Pool

水池是一种记忆这个计划很快,但在美国却很有限。了解更多信息池上的模拟(也称为模拟(分段存储,请参阅概念和简化存储存储。

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

将提供一个演示如何使用内存池的模拟示例。

任何类型的";池";实际上只是您预先获取/初始化的资源,以便它们已经准备就绪,而不是随每个客户端请求动态分配。当客户端完成使用后,资源将返回到池中,而不是被销毁。

内存池基本上只是您预先分配的内存(通常是大块(。例如,您可以提前分配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();
}

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

更有趣的是伙伴分配器、slack、应用拟合算法的分配器等。实现分配器与数据结构没有太大区别,但你会陷入原始比特和字节的膝盖深处,必须考虑对齐等问题,并且不能乱洗内容(不能使现有的指向正在使用的内存的指针无效(。就像数据结构一样,并没有一个真正的黄金标准说;你应该这样做;。它们种类繁多,各有优缺点,但也有一些特别流行的内存分配算法。

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

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

为了实现这一点,您需要自己管理内存使用情况,而不能依赖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参数可以省略,因为编译器允许您在malloc()中调用sizeof(T)

返回一个简单的指针意味着,只有当有相邻内存可用时,池才能增长,只有当池的"边界"内存没有被占用时,池才会收缩。更具体地说,您不能重新定位池,因为这会使malloc函数返回的所有指针失效。

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

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

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

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

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

这样做允许您在执行过程中只进行一次分配(假设您大致知道总共需要多少内存(。如果你的程序是延迟的,而不是内存绑定的,你可以写一个比malloc执行得更快的分配函数,但要牺牲一些内存使用。