• 经典的arena内存池实现-levelDB的内存池实现


    Arena实现

    arena可以说是解决内存碎片的利器,虽然有很多前辈说,要相信malloc的实现,你能想到的那些问题在设计Malloc的时候肯定都考虑到了。是的你可以相信malloc的实现,但是你不能对你自己有过分的自信,在功能比较复杂,特别是工作量比较大的时候,你不能保证你申请的每块内存都得到有效的释放,这个时候就不可避免的出现内存泄露。

    arena解决的问题

    • 频繁分配内存,造成内存碎片化

    • 申请之后忘记释放,造成内存泄露

    C语言接口与设计一书中,详细说明了Arena的设计,levelDB中可以说是简化了Arena的复杂度,只实现了内存的申请,没有实现内存的释放。

    实现细节:

    1. 如果申请内存大于1024字节,直接申请一块指定大小的内存(默认4096),并将内存地址返回

    2. 如果申请的内存小于1024字节,则按照4096大小进行申请,返回指定大小内存,并记录剩余内存的大小,方便下次申请使用

    适用场景:

    Arena在事件处理、流水线处理、请求类型处理中有具有无可无可比拟的优势,事件开始,创建arena,中间过程无论那需要内存,只管申请,申请之后不用担心释放的事情,等到事件结束之后,只需要释放arena句柄就行了,即避免了内存碎片,又避免了内存泄露,同时也减轻了程序员的负担。

    下面是levelDB中Arena实现的头文件

    class Arena {
    public:
        Arena();
    
        Arena(const Arena &) = delete;
    
        Arena &operator=(const Arena &) = delete;
    
        ~Arena();
    
        // Return a pointer to a newly allocated memory block of "bytes" bytes.
        char *Allocate(size_t bytes);
    
        // Allocate memory with the normal alignment guarantees provided by malloc.
        char *AllocateAligned(size_t bytes);
    
        // Returns an estimate of the total memory usage of data allocated
        // by the arena.
        size_t MemoryUsage() const {
            return memory_usage_.load(std::memory_order_relaxed);
        }
    
    private:
        char *AllocateFallback(size_t bytes);
    
        char *AllocateNewBlock(size_t block_bytes);
    
        // Allocation state
        char *alloc_ptr_;
        size_t alloc_bytes_remaining_;
    
        // Array of new[] allocated memory blocks
        std::vector<char *> blocks_;
    
        // Total memory usage of the arena.
        //
        // TODO(costan): This member is accessed via atomics, but the others are
        //               accessed without any locking. Is this OK?
        std::atomic<size_t> memory_usage_;
    };
    
    inline char *Arena::Allocate(size_t bytes) {
        // The semantics of what to return are a bit messy if we allow
        // 0-byte allocations, so we disallow them here (we don't need
        // them for our internal use).
        assert(bytes > 0);
        if (bytes <= alloc_bytes_remaining_) {
            char *result = alloc_ptr_;
            alloc_ptr_ += bytes;
            alloc_bytes_remaining_ -= bytes;
            return result;
        }
        return AllocateFallback(bytes);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    可以看到,只是简单的实现了内存的申请,内存释放需要依赖析枸arena来一次性析枸,但这样已经足够内存数据库这种场景使用了。

    下面来看下函数的具体实现,第一个就是头文件里面实现的Allocate函数

    inline char *Arena::Allocate(size_t bytes) {
        // The semantics of what to return are a bit messy if we allow
        // 0-byte allocations, so we disallow them here (we don't need
        // them for our internal use).
        // 如果允许申请0字节的内存,会造成很多换乱的问题,因此内部接口中我们禁止这种使用方式,bytes要确保大于0
        assert(bytes > 0);
        // 如果上次申请的内存还够用
        if (bytes <= alloc_bytes_remaining_) {
            // 记录当前指针地址
            char *result = alloc_ptr_;
            // 将当前指针向后移动bytes
            alloc_ptr_ += bytes;
            // 剩余的内存大小在原有的基础上要减少 bytes
            alloc_bytes_remaining_ -= bytes;
            // 将地址放回
            return result;
        }
        // 如果上次申请剩余的内存不够,或者第一次进来申请内存,就新申请内存
        return AllocateFallback(bytes);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当申请的内存大于kBlockSize直接申请指定大小的内存,如果小于kBlockSize就一次申请kBlockSize大小的内存,将alloc_ptr_指向本次使用内存的结尾并使用alloc_bytes_remaining_记录剩余可用内存大小

    static const int kBlockSize = 4096;
    
    • 1

    在创建Arena对象的时候,构造函数会将当前申请内存的指针赋值为空,剩余可用内存大小也设置为空,已使用的内存也设置为空,这些值的设置也符合我们常用的规范

    Arena::Arena()
            : alloc_ptr_(nullptr), alloc_bytes_remaining_(0), memory_usage_(0) {}
    
    • 1
    • 2

    在每次需要新申请内存的时候,我们都会把新申请的内存插入到blocks_中,这样在释放对象的析构函数中只需要将所有blocks_中的插入的对象进行释放即可将整个Arena存在期间申请的内存一次性释放干净。

    Arena::~Arena() {
        for (auto & block : blocks_) {
            delete[] block;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    从上面的函数可以看出来,Allocate函数内部真正申请内存调用的是AllocateFallback函数,具体的实现如下:

    char *Arena::AllocateFallback(size_t bytes) {
        if (bytes > kBlockSize / 4) {
            // Object is more than a quarter of our block size.  Allocate it separately
            // to avoid wasting too much space in leftover bytes.
            // 申请的原则
            // 如果申请的内存大于指定block size的四分之一,就按照指定内存大小进行申请
            char *result = AllocateNewBlock(bytes);
            return result;
        }
    
        // We waste the remaining space in the current block.
        // 如果需要的大小小于1024字节,那么按照个4096字节大小申请,
        alloc_ptr_ = AllocateNewBlock(kBlockSize);
        // 申请之后将remaining大小修改为申请的大小
        alloc_bytes_remaining_ = kBlockSize;
    
        char *result = alloc_ptr_;
        // 指针向前移动指定字节
        alloc_ptr_ += bytes;
        // 剩余可用内存减去已经使用的内存
        alloc_bytes_remaining_ -= bytes;
        return result;
    }
    
    char *Arena::AllocateNewBlock(size_t block_bytes) {
        char *result = new char[block_bytes];
        // 将每次new出来的指针方能如到blocks中
        blocks_.push_back(result);
        // 已经使用的内存,是一个指针的大小和指定内存大小的和
        memory_usage_.fetch_add(block_bytes + sizeof(char *),
                                std::memory_order_relaxed);
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    char *Arena::AllocateAligned(size_t bytes) {
        // 如果当前系统指针大于8字节,就按照指针大小进行对齐,如果不是就按照8字节对齐
        const int align = (sizeof(void *) > 8) ? sizeof(void *) : 8;
        // 确保对齐字节大小是2的次方
        static_assert((align & (align - 1)) == 0,
                      "Pointer size should be a power of 2");
        // 看当前的alloc_ptr_是否是8字节对齐
        size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
        // 如果 alloc_ptr_是8字节对齐,那么current_mod会等于0,slop也会是0,,如果slop是alloc_ptr_前进多少能够8字节对齐的位置
        size_t slop = (current_mod == 0 ? 0 : align - current_mod);
        // 实际需要自己的大小为slop和需要申请内存大小的和
        size_t needed = bytes + slop;
        char *result;
        if (needed <= alloc_bytes_remaining_) {
            result = alloc_ptr_ + slop;
            alloc_ptr_ += needed;
            alloc_bytes_remaining_ -= needed;
        } else {
            // AllocateFallback always returned aligned memory
            result = AllocateFallback(bytes);
        }
        // 确保申请内存的指针是8字节对齐
        assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 相关阅读:
    Java学习 --- 面向对象三大特征之封装
    AI四维彩超预测宝宝长相图片生成流量主小程序开发
    JVM 类加载器子系统
    Label 与 Label Selector
    weback5基础配置详解
    Docker使用教程笔记
    信息学奥赛一本通:1157:哥德巴赫猜想
    Consul服务注册与发现
    .ttf 字体剔除
    简洁高性能读服务架构
  • 原文地址:https://blog.csdn.net/andrewgithub/article/details/128093780