26

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

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

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

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

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

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

什么是池?

池分配是一种内存分配方案,速度非常快,但使用受限。有关池分配(也称为简单隔离存储)的更多信息,请参阅概念概念和简单隔离存储。

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

一个显示如何使用内存池的简单示例将不胜感激。

4

3 回答 3

32

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

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

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

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

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

于 2015-05-28T22:57:02.423 回答
10

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

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

第一种方法是定义自己的 Class 封装内存池并提供自定义方法来实现 and 的语义newdelete但从预分配的池中获取内存。请记住,这个池只不过是一个已分配的内存区域,new并且具有任意大小。池的版本new/ deletereturn 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可以省略参数,因为编译器允许您调用sizeof(T).malloc()

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

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

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

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

于 2015-05-28T13:53:41.870 回答
3

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

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

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

于 2015-05-28T13:46:24.083 回答