美文网首页程序员
C++ 实现高性能内存池

C++ 实现高性能内存池

作者: 找不到工作 | 来源:发表于2017-07-20 23:11 被阅读1773次

    简介


    实际上是对开源代码的学习。毕竟水平有限,能搞懂轮子的原理就不错了,不指望能造个更好的轮子。按自己的理解把代码重写了一遍,添加了中文注释。下面放链接。

    1. 原始项目
    2. 我的项目

    原理


    一般而言,我们使用 malloc 或者 new 来动态管理内存。 然而,这些函数由于牵涉到系统调用,效率很低。于是,如果我们使用很少的系统调用,申请一大块内存自己管理,就非常好了。这就是内存池的思想。内存池向系统申请大块内存,然后再分为小片给程序使用。每次我们请求内存,其实是在内存池里拿而非通过系统调用。它的优势体现在:

    1. 它从原理上比 malloc 以及 new
    2. 分配内存时不存在 overhead
    3. 几乎不存在内存碎片
    4. 无须一个一个释放对象,只需要释放内存池即可

    内存池也有缺陷:

    1. 需要知道对象的大小
    2. 需要根据应用的情况来调节

    源码阅读笔记

    = delete 用法

      /*******
      * 赋值 *
      *******/
      //  使用 = delete 禁用拷贝赋值,只保留移动赋值
      MemoryPool& operator=(const MemoryPool& mp) = delete;
      MemoryPool& operator=(const MemoryPool&& mp) noexcept;
    

    参考 stackoverflow 上的相关问题。一句话总结就是禁用函数。
    以上代码实际上就是禁用拷贝赋值,仅使用移动赋值。这是显然的,内存池拷贝代价太大。

    static_assert 用法

      // static_assert: 编译期断言检查
      static_assert(BlockSize >= 2 * sizeof(Slot_), "BlockSize too small.")
    

    在编译期间执行检查,如果不通过条件 arg1,则报错 arg2。
    由于是编译期间运行,因此只能用于编译期间就确定了的常量。尤其常用于模板类的检查。

    使用 union 的理由

        union Slot_ {
          value_type element;
          Slot_* next;
        };
    

    一个奇怪的构造函数

    // MemoryPool.h
      MemoryPool(const MemoryPool& mp) noexcept; // 拷贝构造函数
    
    // MemoryPool.cpp
      template <typename T, size_t BlockSize>
      MemoryPool<T, BlockSize>::MemoryPool(const MemoryPool& mp) noexcept:
      MemoryPool() {
      }
    

    推测其意思就是调用无参数构造函数。

    析构函数中的强制类型转换

    template <typename T, size_t BlockSize>
    MemoryPool<T, BlockSize>::~MemoryPool()
    noexcept
    {
      slot_pointer_ curr = currentBlock_;
      while (curr != nullptr) {
        slot_pointer_ prev = curr->next;
        operator delete(reinterpret_cast<void*>(curr));
        curr = prev;
      }
    }
    

    比较费解为什么要强制转为 void* 指针。源码中多次使用了 reinterpret_cast,具体可以参考 static_cast 和 reinterpret_cast

    内存池实现细节


    直接上图,这个图说明了内存池的运作原理:

    Memory-Pool

    首先定义了一个 union,非常值得注意的是,这不是一个链表。这个 union 的本质是复用,一个 slot 里,既可能存的是数据,也可能存的是指向另一个 slot 的指针。这样的设计有助于节省空间,在某个对象不再需要时,直接放入 freeSlots_ 链表中,并改写 next 指针。

      union Slot_ {
        T element;
        Slot_ *next;
      };
    

    为了实现内存池,我们需要四个指针:

      Slot_ *currentBlock_;
      Slot_ *currentSlot_;
      Slot_ *lastSlot_;
      Slot_ *freeSlots_;  // 这是一个比较让人疑惑的名字,实际是指的 deallocate 之后空闲出来的 slots
    

    结合上面图中的位置,可以大概理解其运作方式。注意图中没有标出 freeSlots_,因为这个指针如果没有进行 deallocate,就保持为 nullptr。

    下面分析最关键的两个函数。

    1. 内存池向系统申请内存
    template <typename T, size_t BlockSize>
    void MemoryPool<T, BlockSize>::allocateBlock() {
      // 头插链表
      // A->B->C 插入 X
      // X->A->B->C
      char *newBlock = reinterpret_cast<char *>(::operator new(BlockSize));
      reinterpret_cast<Slot_ *>(newBlock)->next = currentBlock_;
      currentBlock_ = reinterpret_cast<Slot_ *>(newBlock);
      // 这里需要作一个内存对齐,因为第一个区域存放的是指向上一个 Block 的指针,而不是 Slot
      // 而之后的都是Slot
      char *body = newBlock + sizeof(Slot_ *);
      size_t bodyPadding = padPointer(body, alignof(Slot_));
      currentSlot_ = reinterpret_cast<Slot_ *>(body + bodyPadding);
    
      lastSlot_ =
          reinterpret_cast<Slot_ *>(newBlock + BlockSize - sizeof(Slot_) + 1);
    }
    

    里面的注意点都用中文注释出来了。

    1. 在内存池中为对象分配内存
      inline T *allocate(size_t n = 1, const T *hint = nullptr) {
        if (freeSlots_ != nullptr) {
          // 存在 freeSlots
          T *result = reinterpret_cast<T *>(freeSlots_);
          freeSlots_ = freeSlots_->next;
          return result;
        } else {
          // freeSlots 还未初始化或者已经耗尽
          if (currentSlot_ >= lastSlot_) {
            allocateBlock();
          }
          return reinterpret_cast<T *>(currentSlot_++);
        }
      }
    

    这里尤其需要注意的是,在本文的实现中,freeSlots_只在释放对象时才会被赋值。如果只进行了分配没有释放,那么会一直执行 else 中的代码。

      inline void deallocate(T *p, size_t = 1) {
        if (p != nullptr) {
          // 也是头插链表
          // 空闲链表 A->B->C,现在释放了 X 的空间,需要将 X 加入空闲链表
          // X->A->B->C
          reinterpret_cast<Slot_ *>(p)->next = freeSlots_;
          freeSlots_ = reinterpret_cast<Slot_ *>(p);
        }
      }
    

    在释放中,体现了 union 的作用。传入的指针本来指向一个对象,将其强制转为 union Slot,再将 next 指针赋值为空闲链表头。此时就覆盖对象的前 8 字节内容(64 位机器),对象失效。只留下前 8 字节指针被使用。

    内存池的效果


    运行测试函数可以得到:

    Copyright (c) 2013 Cosku Acay, http://www.coskuacay.com
    Provided to compare the default allocator to MemoryPool.
    
    Default Allocator Time: 7.3624
    
    MemoryPool Allocator Time: 1.23673
    
    Here is a secret: the best way of implementing a stack is a dynamic array.
    Vector Time: 2.14202
    
    The vector implementation will probably be faster.
    

    可以看出,在 OS X 下,明显快于原始的 allocator。奇怪的是我在 Linux 操作系统下并没有跑出这么大的差距,并且 vector 实现确实快于内存池。

    相关文章

      网友评论

        本文标题:C++ 实现高性能内存池

        本文链接:https://www.haomeiwen.com/subject/emnlkxtx.html