前言 本篇文章主要内容为讲述自己对于C++11内存池项目(一位大佬所写)的解析 (注意这里是我记录一下自己对项目的解析)。初次上手项目,很多知识点都没有遇到过,有些地方会提供其他的博文帮助理解,有描述不清楚或存在错误的地方还请大家一一指出。
【源码剖析】MemoryPool —— 简单高效的内存池 allocator 实现
大佬项目源码:GitHub源码
本人详细注释的源码:[**GitHub地址**](CodingRookie98/MemoryPoolPractice (github.com) )
项目介绍 内存池是什么 话不多说,这里摘录最具权威的原作者对于项目的解释: **什么是内存池?什么是 C++ 的 allocator?
内存池简单说,是为了减少频繁使用 malloc/free new/delete 等系统调用而造成的性能损耗而设计的。当我们的程序需要频繁地申请和释放内存时,频繁地使用内存管理的系统调用可能会造成性能的瓶颈,嗯,是可能,毕竟操作系统的设计也不是盖的(麻麻说把话说太满会被打脸的(⊙v⊙))。内存池的思想是申请较大的一块内存(不够时继续申请),之后把内存管理放在应用层执行,减少系统调用的开销。**
allocator详解 那么,allocator 呢?它默默的工作在 C++ 所有容器的内存分配上。默默贴几个链接吧:
C++ allocator
C++学习笔记(十) 内存机制与Allocator
class templatestd::allocator
allocator_traits
当你对 allocator 有基本的了解之后,再看这个项目应该会有恍然大悟的感觉,因为这个内存池是以一个 allocator 的标准来实现的。一开始不明白项目里很多函数的定义是为了什么,结果初步了解了 allocator 后才知道大部分是标准接口。这样一个 memory pool allocator 可以与大多数 STL 容器兼容,也可以应用于你自定义的类。像作者给出的例子 —— test.cpp, 是用一个基于自己写的 stack 来做 memory pool allocator 和 std::allocator 性能的对比 —— 最后当然是 memory pool allocator 更优 。
内存池是一个一个的 block 以链表的形式连接起来,每一个 block 是一块大的内存,当内存池的内存不足的时候,就会向操作系统申请新的 block 加入链表。还有一个 freeSlots_ 的链表,链表里面的每一项都是对象被释放后归还给内存池的空间,内存池刚创建时 freeSlots_> 是空的,之后随着用户创建对象,再将对象释放掉,这时候要把内存归还给内存池,怎么归还呢?就是把指向这个对象的内存的指针加到 freeSlots_ 链表的前面(前插)。 用户在创建对象的时候,先检查 freeSlots_ 是否为空,不为空的时候直接取出一项作为分配出的空间。否则就在当前 block 内取出一个 Slot_ 大小的内存分配出去,如果 block 里面的内存已经使用完了呢?就向操作系统申请一个新的 block。
内存池工作期间的内存只会增长,不释放给操作系统。直到内存池销毁的时候,才把所有的 block delete 掉 。
注释源码: 点我到注释源码 下图左一为刚申请的Blcok↓
MemoryPool.tcc 该文件对Memory.h文件中声明过的函数进行实现,在此一些简单的类函数就不再一一赘述,只挑选出部分函数给出详细剖析,其余可在最后的代码汇总中查看注释。函数中使用了大量的强制转换,这里贴几个有关博客:【C++】四种强制类型转换 c++类型转换
allocateBlock 创建Block块 函数用于申请一块Block内存块,并使用头插法放入Blcok内存块链表。
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 template <typename T, size_t BlockSize>void MemoryPool<T, BlockSize>::allocateBlock () { data_pointer_ newBolck = reinterpret_cast <data_pointer_>(operator new (BlockSize)); reinterpret_cast <slot_pointer_>(newBolck)->next = this ->currentBlock_; currentBlock_ = reinterpret_cast <slot_pointer_>(newBolck); data_pointer_ body = newBolck + sizeof (slot_pointer_); size_type bodyPadding = padPointer (body, alignof (slot_type_)); currentSlot_ = reinterpret_cast <slot_pointer_>(body + bodyPadding); lastSlot_ = reinterpret_cast <slot_pointer_>(newBolck + BlockSize - sizeof (slot_type_) + 1 ); }
这里需要注意的是:在生成一块BlockSize字节大小的Block内存块时需要保留Slot类型大小的空间用于后续Block链表的链接。对应图中的蓝色内存区。此外,在调用padPointer函数 的时候传入的参数使用了 C++11 的新特性 C++11 内存对齐 alignof alignas
padPointer 空间对齐 每一个Block内存块的大小(BlockSize字节)并不是刚好为Slot槽大小的整数倍(取决于使用者定义的BlockSize),例如一个内存块Block大小设置为BlockSize = 11 bytes,每个Slot槽存放数据的大小为4字节时,BlockSize % sizeof(Slot) = 11 % 4 = 2 … 3,余下的三个字节是不足一个Slot槽所需空间的,这时我们必须略去这些多余的空间。上述空间在图中用橙色区域进行了表示。
1 2 3 4 5 6 7 template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::size_typeMemoryPool<T, BlockSize>::padPointer (data_pointer_ p, size_type align) const noexcept { uintptr_t result = reinterpret_cast <uintptr_t >(p); return ((align - result) % result); }
MemoryPool 构造函数 这里说明一下内存池中使用到的指针,先看看原作者是怎么描述的:
1 2 3 4 slot_pointer_ currentBlock_; slot_pointer_ currentSlot_; slot_pointer_ lastSlot_; slot_pointer_ freeSlot_;
当时阅读项目时很不解的点就是 currentSlot 指针的含义,作者给出的解释是元素链表的头指针 ,我思来想去,每一个Slot槽是一个联合体:
1 2 3 4 union Slot_ { value_type element; Slot_* next; };
当Slot用来存放数据时,根据联合的特性,另一个指针属性是不可以使用的,这两个属性的关系也很简单,不可同时出现。因此当Slot槽存储数据时不可能同时是一个链表(链表至少需要一个指针),正是这样,再根据后面对于currentSlot指针的使用我将其解释为 指向第一个可用元素 Slot ,这是狭义上的第一个可用,在后面allocate函数中为每一个元素分配空间的时候currentSlot指针是明确的只能往后走的,也就是说这个 “可用” 不包括已经在空闲链表上的Slot(使用过但现在释放并还给内存池的槽),指向的是从没有被置放过数据的Slot槽。
1 2 3 4 5 6 7 8 template <typename T, size_t BlockSize>MemoryPool<T, BlockSize>::MemoryPool () noexcept { currentBlock_ = nullptr ; currentSlot_ = nullptr ; lastSlot_ = nullptr ; freeSlot_ = nullptr ; }
allocate 内存分配 为每个element元素分配内存,先查询槽的空闲链表是否为空,若不为空则直接将链表头结点表示的空间分配出去,反之到Block中分割新的Slot大小的空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::pointerMemoryPool<T, BlockSize>::allocate (size_type n, constPointer hint) { pointer result; if (freeSlot_ != nullptr ) { result = reinterpret_cast <pointer>(freeSlot_); freeSlot_ = freeSlot_->next; } else { if (currentBlock_ >= lastSlot_) { allocateBlock (); result = reinterpret_cast <pointer>(currentBlock_++); } } return result; }
当 currentSlot 到了 lastSlot 的位置表明该Block即将用完,必须另起炉灶,再向操作系统申请新的Block内存块。这里有一个疑点
1 2 if (currentSlot_ >= lastSlot_) allocateBlock ();
也就是说在 currentSlot 指针等于 lastSlot 的时候调用allocateBlock函数,而在此时这块空间还没有被分配出去,调用函数allocateBlock时我们注意到在函数中currentSlot指针被修改指向了新的内存块Block中的空间了!!!currentSlot_ = reinterpret_cast(body + bodyPadding); 也就是说之前的内存块中最后一个Slot槽大小的空间还未被分配出去就已经改变了currentSlot指针,于是我将if条件修改为了
1 2 if (currentSlot_ > lastSlot_) allocateBlock ();
再次运行时就报错了,,,
deallocate 释放内存 这里的释放内存并不是将这块空间真正的释放掉了(归还给操作系统),而是归还给内存池。这样使得内存池的空间可复用性更高,不需要频繁机械地向OS申请内存,那么这里又是如何将释放掉的Slot槽回收起来呢?考虑到Slot槽并不一定是连续释放的,因此使用链表的形式对这些离散分布的内存槽进行连接,既然是链表,那么必须有指针来完成链表的链接工作,这个指针就大有来头了,回顾一下 Slot 槽的内部结构:
1 2 3 4 union Slot_ { value_type element; Slot_* next; };
既然使用了联合,那么很显然就是用来节省内存的,说到这里可以猜测到槽中的 Slot * 属性是用来充当链表中的指针,当我们创建对象的时候使用 value_type 也就是普通值类型,而销毁数据之后就可以使用它的另一属性将其放入空闲链表中。下图为一块Block块空闲链表的链接情况: deallocate具体操作:
1 2 3 4 5 6 7 8 9 template <typename T, size_t BlockSize>inline void MemoryPool<T, BlockSize>::deallocate (pointer p, size_type n) { if (p != nullptr ) { reinterpret_cast <slot_pointer_>(p)->next = freeSlot_; freeSlot_ = reinterpret_cast <slot_pointer_>(p); } }
使用头插法维护空闲链表,当有空间请求时优先考虑空闲链表,若链表非空,则将链表头指针所指空间分配出去。注意:这里的链表只有一个指针域,其实就是一串指针保存了被释放Slot的地址
max_size 计算最大可用槽的个数 先放上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::size_typeMemoryPool<T, BlockSize>::maxSize () const noexcept { size_type maxBlock = -1 / BlockSize; return (BlockSize - sizeof (data_pointer_) / sizeof (slot_type_) * maxBlock); }
这个函数的作用就是通过计算确定最大可用的Slot的个数,这样可以防止越界导致数据损坏(越界之后的地址由于是循环计数机制会回到起点,对已经存储数据的空间进行破坏),这里的计算方式很有意思,我们一起看看吧
1 size_type maxBlocks = -1 / BlockSize;
这里的 size_type 是被 typedef 过的 unsigned int, BlockSize为 size_t (同无符号整型), 注意这个分数的上部,是不是很有意思,用一个负数去进行运算,最后赋值给无符号的数值类型,先别凌乱,慢慢来分析: 首先计算机内部为了能够更加轻松自然地去进行负数运算,选择原码是万万不可的,打个比方: 32位系统中原码的表示为 最高位为符号位,其余31位为数值位。 若使用原码进行计算: 2 + (-1)竟然等于 -3!!!很明显这不可行。最终科学家们找到一种很实用的方式就是补码运算使用补码的好处 负数的补码等于其反码加1。 求得-1的补码其实就是 unsigned int 类型可以表示的最大值,那么 maxBlock 表示的就是内存池最多能生成的内存块Block的数量。而返回的是一个表达式: return (BlockSize - sizeof(data_pointer_)) / sizeof(slot_type_) * maxBlocks; BlockSize - sizeof(data_pointer) 表示每一个Block块除去一个指针大小之后的可用空间,“ (BlockSize - sizeof(data_pointer_)) / sizeof(slot_type_) ”表示一个Block块真正可以使用来存储数据的Slot槽的个数,每一块Block可以用的槽个数再乘上内存块的最大个数即可得到一个内存池可以使用的Slot槽的个数。 后续还有一些基本的类函数就不再一一介绍了,代码中含有注释。
~MemoryPool 析构函数 这里很有操作的是将 Slot* 类型指针转换为 void* 类型再对其释放空间,原解释是 void*类型释放不需要析构函数;
如果void 指向一个class类,那么系统由于认为void 指向一个普通的内存空间,所以释放指针时系统class的析构函数不会调用 转自 释放void*指针
1 2 3 4 5 6 7 8 9 10 11 12 13 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; } }
MemoryPool头文件 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #ifndef MEMORY_POOL_H #define MEMORY_POOL_H #include <climits> #include <cstddef> template <typename T, size_t BlockSize = 4096 >class MemoryPool {public : typedef T value_type; typedef T* pointer; typedef T& reference; typedef const T* constPointer; typedef const T& constReference; typedef size_t size_type; typedef ptrdiff_t differenceType; typedef std::false_type propagate_on_container_copy_assinment; typedef std::true_type propagate_on_container_move_assinment; typedef std::true_type propagate_on_container_move_swap; template <typename U> struct rebind { typedef MemoryPool<U> other; }; MemoryPool () noexcept ; MemoryPool (const MemoryPool& memoryPool) noexcept ; MemoryPool (MemoryPool&& memoryPool) noexcept ; template <class U > MemoryPool (const MemoryPool<U>& MemoryPool) noexcept ; ~MemoryPool () noexcept ; MemoryPool& operator =(const MemoryPool& memoryPool) = delete ; MemoryPool& operator =(MemoryPool&& MemoryPool) noexcept ; pointer address (reference x) const noexcept ; constPointer address (constReference x) const noexcept ; pointer allocate (size_type n = 1 , constPointer hint = nullptr ) ; void deallocate (pointer p, size_type n = 1 ) ; size_type maxSize () const noexcept ; template <class U , class ... Args> void construct (U* p, Args&&... args) ; template <class U > void destroy (U* p) ; template <class ... Args> pointer newElement (Args&&... args) ; void deleteElement (pointer p) ; private : union Slot_ { value_type element; Slot_* next; }; typedef char * data_pointer_; typedef Slot_ slot_type_; typedef Slot_* slot_pointer_; slot_pointer_ currentBlock_; slot_pointer_ currentSlot_; slot_pointer_ lastSlot_; slot_pointer_ freeSlot_; size_type padPointer (data_pointer_ p, size_type align) const noexcept ; void allocateBlock () ; static_assert (BlockSize >= 2 * sizeof (size_type), "BlockSize too small." ); }; #include "./MemoryPool.tcc" #endif
函数实现MemoryPool.tcc 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 #ifndef MEMORY_POOL_TCC #define MEMORY_POOL_TCC #include "MemoryPool.h" template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::size_typeMemoryPool<T, BlockSize>::padPointer (data_pointer_ p, size_type align) const noexcept { uintptr_t result = reinterpret_cast <uintptr_t >(p); return ((align - result) % result); } template <typename T, size_t BlockSize>MemoryPool<T, BlockSize>::MemoryPool () noexcept { currentBlock_ = nullptr ; currentSlot_ = nullptr ; lastSlot_ = nullptr ; freeSlot_ = nullptr ; } template <typename T, size_t BlockSize>MemoryPool<T, BlockSize>::MemoryPool (const MemoryPool& memoryPool) noexcept : MemoryPool () {}template <typename T, size_t BlockSize>MemoryPool<T, BlockSize>::MemoryPool (MemoryPool&& memoryPool) noexcept { this ->currentBlock_ = memoryPool.currentBlock_; memoryPool.currentBlock_ = nullptr ; this ->currentSlot_ = memoryPool.currentSlot_; this ->lastSlot_ = memoryPool.lastSlot_; this ->freeSlot_ = memoryPool.freeSlot_; } template <typename T, size_t BlockSize>template <class U >MemoryPool<T, BlockSize>::MemoryPool (const MemoryPool<U>& memoryPool) noexcept : MemoryPool () {}template <typename T, size_t BlockSize>MemoryPool<T, BlockSize>& MemoryPool<T, BlockSize>::operator =(MemoryPool&& memoryPool) noexcept { if (this != &memoryPool) { std::swap (this ->currentBlock_, memoryPool.currentBlock_); this ->currentSlot_ = memoryPool.currentSlot_; this ->lastSlot_ = memoryPool.lastSlot_; this ->freeSlot_ = memoryPool.freeSlot_; } } 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; } } template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::pointerMemoryPool<T, BlockSize>::address (reference x) const noexcept { return &x; } template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::constPointerMemoryPool<T, BlockSize>::address (constReference x) const noexcept { return &x; } template <typename T, size_t BlockSize>void MemoryPool<T, BlockSize>::allocateBlock () { data_pointer_ newBolck = reinterpret_cast <data_pointer_>(operator new (BlockSize)); reinterpret_cast <slot_pointer_>(newBolck)->next = this ->currentBlock_; currentBlock_ = reinterpret_cast <slot_pointer_>(newBolck); data_pointer_ body = newBolck + sizeof (slot_pointer_); size_type bodyPadding = padPointer (body, alignof (slot_type_)); currentSlot_ = reinterpret_cast <slot_pointer_>(body + bodyPadding); lastSlot_ = reinterpret_cast <slot_pointer_>(newBolck + BlockSize - sizeof (slot_type_) + 1 ); } template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::pointerMemoryPool<T, BlockSize>::allocate (size_type n, constPointer hint) { pointer result; if (freeSlot_ != nullptr ) { result = reinterpret_cast <pointer>(freeSlot_); freeSlot_ = freeSlot_->next; } else { if (currentBlock_ >= lastSlot_) { allocateBlock (); result = reinterpret_cast <pointer>(currentBlock_++); } } return result; } template <typename T, size_t BlockSize>inline void MemoryPool<T, BlockSize>::deallocate (pointer p, size_type n) { if (p != nullptr ) { reinterpret_cast <slot_pointer_>(p)->next = freeSlot_; freeSlot_ = reinterpret_cast <slot_pointer_>(p); } } template <typename T, size_t BlockSize>inline typename MemoryPool<T, BlockSize>::size_typeMemoryPool<T, BlockSize>::maxSize () const noexcept { size_type maxBlock = -1 / BlockSize; return (BlockSize - sizeof (data_pointer_) / sizeof (slot_type_) * maxBlock); } template <typename T, size_t BlockSize>template <class U , class ... Args>inline void MemoryPool<T, BlockSize>::construct (U* p, Args&&... args) { new (p) U (std::forward<Args>(args)...); } template <typename T, size_t BlockSize>template <class U >inline void MemoryPool<T, BlockSize>::destroy (U* p) { p->~U (); } template <typename T, size_t BlockSize>template <class ... Args>inline typename MemoryPool<T, BlockSize>::pointerMemoryPool<T, BlockSize>::newElement (Args&&... args) { pointer result = allocate (); construct <value_type>(result, std::forward<Args>(args)...); return result; } template <typename T, size_t BlockSize>inline void MemoryPool<T, BlockSize>::deleteElement (pointer p) { if (p != nullptr ) { p->~value_type (); deallocate (p); } } #endif
写在最后 若有错误及不详尽恳请指出,谢谢!
文章参考:
C++11实现高效内存池 - nepu_bin - 博客园 (cnblogs.com)
【源码剖析】MemoryPool —— 简单高效的内存池 allocator 实现_一线涯的博客-CSDN博客