固定内存块尺寸的内存池原理及代码

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了固定内存块尺寸的内存池原理及代码相关的知识,希望对你有一定的参考价值。

有些情形会需要申请大量的固定尺寸的内存块,若一个个都用malloc申请效率很低,这种情况非常适合使用内存池解决。

下面就是一个固定内存块尺寸的内存池的完整源码。注:其中的内存申请不是使用的malloc,而是自己定义的torch::HeapMalloc,简单修改下即可。

代码详情请见Github【点击】

先看代码再看讲解

/*
 * 固定尺寸的内存池
 * 说明:
 *  - 只适用于需要大量固定尺寸内存块的情况
 */
template<size_t _Size>
class FixedMemoryPool
{
public:
    enum { _CHUNK_COUNT = 1024/_Size };

    FixedMemoryPool();
    ~FixedMemoryPool();

    FixedMemoryPool(const FixedMemoryPool &);
    FixedMemoryPool& operator=(const FixedMemoryPool &) = delete;

    /*
     * 从内存池获得一个内存块
     * 注意:
     *  - 用完后最好在放回内存池中(有利于提高性能)
     *  - 不能使用::free()释放内存,内存池会统一释放池中的内存块
     */
    void* Alloc();
    
    /*
     * 将内存块放回内存池中
     * 注意:必须是从内存池申请的内存块才能放回去
     */
    void Free(void *mem);

    /*
     * 清空内存池
     * 注意:调用此接口后,通过Alloc申请的内存块会全部被释放
     */
    void Clear();

    /*
     * 获得内存池总大小(单位Byte)
     */
    size_t GetCapacity();

    /*
     * 获得内存块的大小(单位Byte)
     */
    size_t GetItemSize();

private:
    struct Chunk;
    Chunk* MakeChunk();

private:
    union ChunkItem {
        ChunkItem*  next;
        uint8_t     mem[_Size];
    };
    struct Chunk { ChunkItem itemArray[_CHUNK_COUNT]; };

    ChunkItem*          m_rootItem;
    std::list<Chunk *>  m_chunkList;
};

template<size_t _Size>
FixedMemoryPool<_Size>::FixedMemoryPool() {
    m_rootItem = nullptr;
    Chunk *chunk = this->MakeChunk();
    if (!chunk)
        return;
    m_chunkList.push_back(chunk);
    m_rootItem = chunk->itemArray;
}

template<size_t _Size>
FixedMemoryPool<_Size>::~FixedMemoryPool() {
    this->Clear();
}

template<size_t _Size>
FixedMemoryPool<_Size>::FixedMemoryPool(const FixedMemoryPool &) {
    m_rootItem = nullptr;
    Chunk *chunk = this->MakeChunk();
    if (!chunk)
        return;
    m_chunkList.push_back(chunk);
    m_rootItem = chunk->itemArray;
}

template<size_t _Size>
void* FixedMemoryPool<_Size>::Alloc() {
    if (!m_rootItem) {
        Chunk *chunk = this->MakeChunk();
        if (!chunk) return nullptr;
        m_chunkList.push_back(chunk);
        m_rootItem = chunk->itemArray;
    }
    void *item = m_rootItem;
    m_rootItem = m_rootItem->next;
    memset(item, 0, _Size);
    return item;
}

template<size_t _Size>
void FixedMemoryPool<_Size>::Free(void *mem) {
    if (!mem) return;
    ChunkItem *item = static_cast<ChunkItem*>(mem);
    item->next = m_rootItem;
    m_rootItem = item;
}

template<size_t _Size>
void FixedMemoryPool<_Size>::Clear() {
    for (Chunk *chunk : m_chunkList) {
        torch::HeapFree(chunk);
    }
    m_chunkList.clear();
}

template<size_t _Size>
size_t FixedMemoryPool<_Size>::GetCapacity() {
    return m_chunkList.size() * _CHUNK_COUNT * _Size;
}

template<size_t _Size>
size_t FixedMemoryPool<_Size>::GetItemSize() {
    return _Size;
}

template<size_t _Size>
typename FixedMemoryPool<_Size>::Chunk* FixedMemoryPool<_Size>::MakeChunk() {
    Chunk *chunk = (Chunk *)torch::HeapMalloc(sizeof(Chunk));
    if (!chunk)
        return nullptr;
    for (int i = 0; i < _CHUNK_COUNT - 1; i++) {
        chunk->itemArray[i].next = &(chunk->itemArray[i+1]);
    }
    chunk->itemArray[_CHUNK_COUNT-1].next = nullptr;
    return chunk;
}

 

首先要清除这个内存池的结构:

 

ChunkList[
    Chunk[ChunkItem, ... ChunkItem],
    Chunk[ChunkItem, ... ChunkItem],
    ...
    Chunk[ChunkItem, ... ChunkItem]
]

 

ChunkItem非常重要,他是一个union,其中有两个成员:

    union ChunkItem {
        ChunkItem*  next; // 指向下一个ChunkItem
        uint8_t     mem[_Size]; // _Size为模板参数
    };
  
struct Chunk { ChunkItem itemArray[_CHUNK_COUNT]; }; 

 

当ChunkItem在池中时,只会使用next域,使用next将一个Chunk中的所有ChunkItem串联成一个链表。

到这里有人发现了,Chunk里面不是一个ChunkItem的数组吗,干毛还用next串联成链表。

我想说的是,此中有深意呀。首先这样更加灵活,当然主要还和内存块的回收有关系,内存块回收回来时这个ChunkItem的位置不确定(在数组中的位置,处于哪一个Chunk),所以直接将他串到链表头就可以了,没必要关心。所以这个Chunk只是刚初始化时是数组和链表等同,后面经过Alloc和Free后,链表和数组就不等同了。

mem字段比较简单了,由末班参数_Size控制其大小,一般也就是ChunkItem的大小(_Size若大于4),ChunItem本身就是分配给用户使用的内存块,尺寸也是用户通过模板参数传入的_Size

 


 

    ChunkItem*          m_rootItem;
    std::list<Chunk *>  m_chunkList;

 

先说m_rootItem,其实就是指向可用的ChunkItem的指针,当Alloc申请时就可以非常简单的

    void *item = m_rootItem;
    m_rootItem = m_rootItem->next;
    memset(item, 0, _Size);
    return item;

 

m_chunkList,其实就是Chunk的记录list,主要用于内存释放,因为一个Chunk的内存块数固定,所以当内存块被申请完之后,就重新构造一个Chunk出来,然后将所有Chunk都保存到m_chunkList。这样就可以:

    for (Chunk *chunk : m_chunkList) {
        torch::HeapFree(chunk);
    }
    m_chunkList.clear();

 

 

下面看看比较关键的Alloc的实现:

template<size_t _Size>
void* FixedMemoryPool<_Size>::Alloc() {
    if (!m_rootItem) { // 当m_rootItem为空,代表没有可用的内存块了。 此时需要重新构造一个Chunk出来
        Chunk *chunk = this->MakeChunk(); // 构造一个Chunk出来,详情见下面
        if (!chunk) return nullptr;
        m_chunkList.push_back(chunk); // 将新构造的Chunk,记录到list中,否则会造成内存泄露
        m_rootItem = chunk->itemArray; // 让m_rootItem指向,新构造的Chunk的ItemArray中的第一个元素
    }
    // 直接将m_rootItem返回,然后将m_rootItem指向下一个
    void *item = m_rootItem;
    m_rootItem = m_rootItem->next;
    memset(item, 0, _Size);
    return item;
}

 

MakeChunk的细节:

template<size_t _Size>
typename FixedMemoryPool<_Size>::Chunk* FixedMemoryPool<_Size>::MakeChunk() {
    Chunk *chunk = (Chunk *)torch::HeapMalloc(sizeof(Chunk)); // 申请一个Chunk的内存块,struct Chunk {ChunkItem itemArray[_CHUNK_COUNT];};
    if (!chunk)
        return nullptr;
    for (int i = 0; i < _CHUNK_COUNT - 1; i++) { // 链表结构的初始化,将头一个指向下一个ChunkItem
        chunk->itemArray[i].next = &(chunk->itemArray[i+1]);
    }
    chunk->itemArray[_CHUNK_COUNT-1].next = nullptr; // 注意!!!最后一个一定要指向null,这样最后一个被申请后,m_rootItem就变为null了
    return chunk;
}

  

Free比较简单:

template<size_t _Size>
void FixedMemoryPool<_Size>::Free(void *mem) {
    if (!mem) return;
    ChunkItem *item = static_cast<ChunkItem*>(mem); 
    item->next = m_rootItem; // 直接将内存块放到链表头
    m_rootItem = item;
}

 

固定尺寸的内存池实现并不复杂,但是却非常有用,Alloc和Free都只是移动一个指针就可以完成内存的申请和释放(在不触发MakeChunk的情况下),非常高效。

这里只介绍了内存池的核心算法,其他地方都比较简单。

 

以上是关于固定内存块尺寸的内存池原理及代码的主要内容,如果未能解决你的问题,请参考以下文章

Spark Shuffle 中 JVM 内存使用及配置内幕详情

nginx源代码分析之内存池实现原理

Python小数据池,代码块解析

深入剖析python小数据池,代码块

python之路(内存,小数据池,编码等)

[Spark性能调优] 第四章 : Spark Shuffle 中 JVM 内存使用及配置内幕详情