基于 Linux、C++实现的高性能内存池

发布时间 2023-10-07 23:30:43作者: 风中凌乱的猪头

1.引入内存池的意义

    内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,但是这种方式非常容易产生内存碎片,早晚都会申请内存失败。并且在比较复杂的代码或者继承的屎山中,非常容易出现内存泄漏。

    内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是能够使内存分配效率得到提升。

2.内存池的实现原理

  我们通过一个内存池类来操作内存池,这个类中的所有成员方法都是操作内存池的函数。在我们设想的内存池中,我们以4k为基准定义,申请空间超过4k的我们额外使用malloc分配一个自定义块,并且使用一个管理自定义块的对象来保存它的地址,以便于在释放空间的时候我们能够找到他。申请空间不超过4k的我们就将我们内存池的标准块中的一部分划分给它来减少malloc的次数,若当前标准块的空间不够,则额外开辟一块4k的空间加入内存池并且给它分配空间。

2.1 内存池实现的主要功能

  在此,笔者将简述一下内存池类(Mem_pool)的成员函数及其实现的主要功能:

Mem_pool(size_t size) :构造函数,创建内存池,并初始化一块自定义大小的空间

~Mem_pool() :析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放

mp_new(size_t size) :给用户提供的申请内存空间的接口

mp_new_zero(size_t size) :给用户提供的申请内存空间的接口,并全部初始化为0

mp_delete(void *p) :给用户提供的释放已申请的内存空间的接口

mp_reset_pool() :给用户提供的清空内存池中数据的接口

mp_monitor(string msg) :给用户提供的显示当前内存池使用情况的接口

mp_new_block(size_t size) :向堆空间申请一块标准块空间

mp_new_large(size_t size) :向堆空间申请一块长度大于标准块长度的自定义大块空间

2.2 内存池空间分配的实现原理

在管理内存池的类(Mem_pool)中,存在着两个链表,其中一个是以管理标准块的类为数据域的不带表头结点的单链表,另一个是以管理自定义块的类为数据域的不带表头的单链表。

    一个管理标准快的对象(Mem_node)管理一个标准块,且该对象的起始地址为该标准块的起始地址。Mem_node类有一个起始指针,一个结束指针与一个next指针,起始指针指向的是该标准块能够分配内存的起始地址,结束指针指向该标准块的结束地址,next指针用来指向下一个Mem_node对象用来形成链表。

    一个管理自定义块的对象(Mem_large)管理一个自定义块,每当我们创建一个自定义块的时候我们才创建一个Mem_large对象来管理它,并且该对象在内存池的标准块中申请空间(换句话说就是Mem_large对象的内存空间由内存池的标准快分配)。Mem_large类有一个指向自定义块的指针,用来以后释放空间,还有一个next指针用来形成链表。

    每当我们需要申请一块空间的时候,我们首先需要判断这块空间的大小是否大于一个标准块的大小(4k),若没有超过我们就将在标准块中分配这段空间。

    假设现在我们在标准块中分配,我们还需要判断我们当前的标准块剩余的可用空间能否容纳下我们需要分配的空间。若可以,我们就将在当前的标准块中划分出空间来实现空间分配,并且减少当前标准块可以分配的空间,如图1所示。若不可以,我们将开辟出一块新的标准块,并实现上述操作,如图2所示。

图片

图1 标准块剩余空间足够的情况

图片

图2 标准块剩余空间不足需要开辟新的标准块的情况

 

假设我们需要分配的空间超过了4k,我们就需要开辟出一个能够容纳该空间的自定义块,将自定义块的内存空间分配出去,接着我们还需要创建一个管理自定义块的对象,这个对象的一个成员指针保存这段被分配的空间的地址用来便于以后释放,如图3所示。

图片

图3 创建自定义块的情况

随着标准块的创建,我们也将创建一个管理该标准块的对象(Mem_node),并将其挂载在管理内存池的对象(Mem_pool)的链表中,所以我们只需要找到内存池就可以找到所有在内存池中分配的标准块。

    随着自定义块的创建,该管理该自定义块的对象将被挂载在管理内存池的对象(Mem_pool)的链表中,所以我们只需要找到内存池就可以找到所有在内存池中分配的自定义块。

2.3 内存池空间释放的实现原理

  当我们想要释放一段从内存池中申请的内存空间时,我们需要判断这段空间是存在在标准块中还是自定义块中。

    若存在于自定义块中,我们将遍历内存池对象中的管理自定义块对象的链表,找到对应的Mem_large对象后,我们只需要释放该对象中指向自定义块的指针就可以实现自定义块空间的释放了。

   若存在于标准块中,我们也需要遍历内存池对象中管理标准块对象的链表,找到对应的Mem_node对象后,我们需要将该对象的引用计数(m_quote)减一(这样的话就可以当作该内存空间已经被释放掉了)。此处需要补充一下,在我们在标准块中申请空间成功时,我们将在增加管理该块的对象中的引用计数,所以该对象的引用计数就是我们在该标准块中申请的内存空间个数。当该块的引用计数为0的时候,我们就可以把这个标准块当作一个刚刚创建的标准块来给其他申请空间的指针来分配空间了。

   提醒:内存池的释放仅进行内存空间的释放,为了避免错误操作内存导致程序错误的情况,请一定记得在释放完空间之后将指针置为nullptr。

3、 对new与delete的重载

  在C++中我们可以对new与delete进行重载实现对堆空间内存申请操作的接管。 这样的话我们就可以几乎感觉不到任何区别地来使用内存池来申请与释放内存空间了。需要另外提示的一点是我们在对new重载之后,依然可以在new后面对类类型进行初始化从而进入到类的构造函数中,例如:Test * p = new Test(10);

void *operator new(size_t size)
{
    return mp.mp_new(size);
}
 
void operator delete(void *p)
{
    mp.mp_delete(p);
}

4. C++内存池对纯C内存池的增强

    在C++内存池中,我们将内存池封装成了一个对象,通过这个对象来操作内存池,通过提供成员函数接口的操作隐藏了具体的实现代码,可以更加安全与简洁地操作内存池。

    另外,将内存池封装成类还可以遵循RAII原则,我们在内存池对象的构造函数里面对内存池进行初始化,在内存池的析构函数里面销毁内存池并且将内存池从内存中申请的所有空间释放,避免了内存泄漏的情况发生。

5、测试如下

class Test
{
public:
    Test() = delete;
    Test(int a, double b, string c);
 
private:
    int m_a;
    double m_b;
    string m_c;
};
 
Test::Test(int a, double b, string c) : m_a(a), m_b(b), m_c(c)
{
    cout << "Test构造!\n";
}
 
int main()
{
    char * p = nullptr;
    string s1 = "show";
    mp.mp_monitor(s1);
    for(int i =0 ;i <100;i++)
    {
         p = (char *)mp.mp_new(50);
    }
    mp.mp_monitor(s1);
    for(int i =0 ;i <20;i++)
    {
         p = (char *)mp.mp_new(60);
    }
    mp.mp_monitor(s1);
    p = (char *)mp.mp_new(5000);
    mp.mp_monitor(s1);
    mp.mp_delete(p);
    mp.mp_monitor(s1);
    Test * p1 = new Test(1,2.0,s1);
    mp.mp_monitor(s1);
 
    return 0;
}

6、源码

  1 #include <iostream>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <string>
  5  
  6 #define PAGE_SIZE 4096
  7  
  8 using uchar = unsigned char;
  9 using namespace std;
 10  
 11 //对齐规则
 12 #define MP_ALIGNMENT 16
 13 #define mp_align(n, alignment) (((n) + (alignment - 1)) & ~(alignment - 1))
 14 #define mp_align_ptr(p, alignment) (void *)((((size_t)p) + (alignment - 1)) & ~(alignment - 1))
 15  
 16  
 17 class Mem_node
 18 {
 19 public:
 20     //初始化node结点
 21     void init();
 22     //将node结点置回初始状态
 23     void node_clean();
 24     //分配成功,引用计数+1,移动start指针
 25     void node_malloc(size_t size);   
 26     //减少引用计数,若为0移动start指针
 27     void node_free();  
 28     uchar * get_start();
 29     uchar * get_end();
 30     int get_quote();
 31     //获取end - start的值,也就是当前块现在的可用长度
 32     size_t get_len();  
 33     Mem_node *get_next();
 34     //每次调用get方法failed计数器自增1
 35     int get_failed(); 
 36     void set_next(Mem_node* next);
 37  
 38 private:
 39     uchar *m_start;  //标记当前标准块可以使用的起始地址
 40     uchar *m_end;   //标记当前标准块可以使用的末尾地址
 41     Mem_node *m_next;   //标准快链表的next指针
 42     int m_quote;       //当前标准块的引用计数器,用来判断当前标准块是否被使用
 43     int m_failed;        //当前标准块的错误计数器,用来移动内存池的current指针提高效率
 44 };
 45  
 46  
 47 class Mem_large
 48 {
 49 public:
 50     //使large结点的成员变量重置为参数所示的值
 51     void large_reset(int size,void * alloc);
 52     //释放掉large结点指向的自定义块内存空间
 53     void large_free();
 54     void set_next(Mem_large * next);
 55     Mem_large * get_next();
 56     void * get_alloc();
 57     int get_size();
 58 private:
 59     Mem_large *m_next;   //自定义块链表的next指针
 60     int m_size;                       //用来存放自定义块所占用的内存大小
 61     void *m_alloc;               //用来存放自定义块所在的起始地址
 62 };
 63  
 64  
 65 class Mem_pool
 66 {
 67 public:
 68     Mem_pool() = delete;
 69  
 70     //构造函数,创建内存池,并初始化一块自定义大小的空间
 71     explicit Mem_pool(size_t size); //类型转化构造函数,自定义的大小仅允许是4k的整数倍
 72  
 73     //析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
 74     ~Mem_pool();
 75  
 76     //给用户提供的申请内存空间的接口
 77     void *mp_new(size_t size);
 78  
 79     //给用户提供的申请内存空间的接口,并全部初始化为0
 80     void *mp_new_zero(size_t size);
 81  
 82     //给用户提供的释放已申请的内存空间的接口
 83     void mp_delete(void *p);
 84  
 85     //给用户提供的清空内存池中数据的接口
 86     void mp_reset_pool();
 87  
 88     //给用户提供的显示当前内存池使用情况的接口
 89     void mp_monitor(string msg);
 90  
 91 private:
 92     //向堆空间申请一块标准块空间
 93     void *mp_new_block(size_t size);
 94  
 95     //向堆空间申请一块长度大于标准块长度的自定义大块空间
 96     void *mp_new_large(size_t size);
 97  
 98     Mem_node *m_head;    //标准块链表的头指针
 99     Mem_node *m_current; //标准块链表当前可能使用的指针(局部性原理)
100     Mem_large *m_large;  //大块链表的头指针
101 };
102  
103 Mem_pool mp(4096);
104  
105 void *operator new(size_t size)
106 {
107     return mp.mp_new(size);
108 }
109  
110 void operator delete(void *p)
111 {
112     mp.mp_delete(p);
113 }
114  
115  
116  
117  
118 void Mem_node::init()
119 {
120     m_start = (uchar *)this + sizeof(Mem_node);
121     m_end = (uchar *)this + PAGE_SIZE;
122     m_next = nullptr;
123     m_failed = 0;
124     m_quote = 0;
125 }
126  
127 //将node结点置回初始状态
128 void Mem_node::node_clean()
129 {
130     m_start = (uchar *)this + sizeof(Mem_node);
131     m_failed = 0;
132     m_quote = 0;
133 }
134  
135 //分配成功,引用计数+1,移动start指针
136 void Mem_node::node_malloc(size_t size)
137 {
138     m_quote++;
139     m_start += size;
140 }
141  
142 //减少引用计数,若为0移动start指针
143 void Mem_node::node_free()
144 {
145     m_quote--;
146 }
147  
148 uchar *Mem_node::get_start()
149 {
150     return m_start;
151 }
152  
153 uchar *Mem_node::get_end()
154 {
155     return m_end;
156 }
157  
158 int Mem_node::get_quote()
159 {
160     return m_quote;
161 }
162  
163 //获取end - start的值,也就是当前块现在的可用长度
164 size_t Mem_node::get_len()
165 {
166     return (m_end - m_start);
167 }
168  
169 Mem_node *Mem_node::get_next()
170 {
171     return m_next;
172 }
173  
174 //每次调用get方法failed计数器自增1
175 int Mem_node::get_failed()
176 {
177     m_failed++;
178     return m_failed;
179 }
180  
181 void Mem_node::set_next(Mem_node *next)
182 {
183     m_next = next;
184 }
185  
186 void Mem_large::large_reset(int size, void *alloc)
187 {
188     m_size = size;
189     m_alloc = alloc;
190 }
191  
192 void Mem_large::large_free()
193 {
194     free(m_alloc);
195     m_size = 0;
196     m_alloc = nullptr;
197 }
198  
199 void Mem_large::set_next(Mem_large *next)
200 {
201     m_next = next;
202 }
203  
204 Mem_large *Mem_large::get_next()
205 {
206     return m_next;
207 }
208  
209 void *Mem_large::get_alloc()
210 {
211     return m_alloc;
212 }
213  
214 int Mem_large::get_size()
215 {
216     return m_size;
217 }
218  
219 //构造函数,创建内存池,并初始化一块自定义大小的空间
220 Mem_pool::Mem_pool(size_t size)
221 {
222     if (size < PAGE_SIZE || size % PAGE_SIZE != 0)
223     {
224         size = PAGE_SIZE;
225     }
226     //posix_memalign((void **)&pool, MP_ALIGNMENT, size);
227     //m_current = (Mem_node *)malloc(size);
228     posix_memalign((void **)&m_current, MP_ALIGNMENT, size);
229     m_head = m_current;
230     m_head->init();
231     m_large = nullptr;
232 }
233  
234 //析构函数,销毁内存池,遍历各个标准块与大块,把从堆区分配的内存释放
235 Mem_pool::~Mem_pool()
236 {
237     Mem_large *large = m_large;
238     Mem_node *cur, *next;
239  
240     for (large; large != nullptr; large = large->get_next())
241     {
242         if (large->get_alloc() != nullptr)
243         {
244             free(large->get_alloc());
245         }
246     }
247  
248     cur = m_head;
249     while (cur != nullptr)
250     {
251         next = cur->get_next();
252         free(cur);
253         cur = next;
254     }
255 }
256  
257 //给用户提供的申请内存空间的接口
258 void *Mem_pool::mp_new(size_t size)
259 {
260     if (size <= 0)
261     {
262         return nullptr;
263     }
264     else if (size >= PAGE_SIZE - sizeof(Mem_node))
265     {
266         return mp_new_large(size);
267     }
268     else
269     {
270         uchar *addr = nullptr;
271         Mem_node *cur = m_current;
272  
273         while (cur != nullptr)
274         {
275             if (cur->get_len() >= size)
276             {
277                 addr = cur->get_start();
278                 cur->node_malloc(size);
279                 return addr;
280             }
281             else
282             {
283                 cur = cur->get_next();
284             }
285         }
286         return mp_new_block(size);
287     }
288 }
289  
290 //给用户提供的申请内存空间的接口,并全部初始化为0
291 void *Mem_pool::mp_new_zero(size_t size)
292 {
293     void *mem_addr = mp_new(size);
294     if (mem_addr != nullptr)
295     {
296         memset(mem_addr, 0, size);
297     }
298     return mem_addr;
299 }
300  
301 //给用户提供的释放已申请的内存空间的接口
302 void Mem_pool::mp_delete(void *p)
303 {
304     Mem_large *large = m_large;
305     Mem_node *node = nullptr;
306  
307     //若需要删除的是自定义块
308     for (large; large != nullptr; large = large->get_next())
309     {
310         if (p == large->get_alloc())
311         {
312             large->large_free();
313             return;
314         }
315     }
316  
317     //若要删除的是标准块
318     node = m_head;
319     for (node; node != nullptr; node = node->get_next())
320     {
321         if (((uchar *)node <= (uchar *)p) && ((uchar *)p <= (uchar *)node->get_end()))
322         {
323             node->node_free();
324             if (node->get_quote() == 0)
325             {
326                 m_current = m_head;
327             }
328             return;
329         }
330     }
331 }
332  
333 //给用户提供的清空内存池中数据的接口
334 void Mem_pool::mp_reset_pool()
335 {
336     Mem_node *node = nullptr;
337     Mem_large *large = nullptr;
338  
339     for (large = m_large; large != nullptr; large = large->get_next())
340     {
341         if (large->get_alloc() != nullptr)
342         {
343             free(large->get_alloc());
344         }
345     }
346  
347     for (node = m_head; node != nullptr; node = node->get_next())
348     {
349         node->node_clean();
350     }
351     m_current = m_head;
352 }
353  
354 //给用户提供的显示当前内存池使用情况的接口
355 void Mem_pool::mp_monitor(string msg)
356 {
357     Mem_node *head = nullptr;
358     Mem_large *large = nullptr;
359     int i = 0;
360  
361     printf("\r\n\r\n------start monitor poll------%s\r\n\r\n", msg.c_str());
362     for (head = m_head; head != nullptr; head = head->get_next())
363     {
364         i++;
365         if (m_current == head)
366         {
367             cout << "current==>第" << i << "块\n";
368         }
369  
370         printf("第%02d块small block  已使用:%4ld  剩余空间:%4ld  引用:%4d  failed:%4d\n", i,
371                head->get_start() - (uchar *)head,
372                head->get_len(), head->get_quote(), head->get_failed());
373     }
374  
375     i = 0;
376     for (large = m_large; large != nullptr; large = large->get_next())
377     {
378         i++;
379         if (large->get_alloc() != nullptr)
380         {
381             cout << "" << i << "块large block  size=" << large->get_size() << "\n";
382         }
383     }
384     printf("\r\n\r\n------stop monitor poll------\r\n\r\n");
385 }
386  
387 //向堆空间申请一块标准块空间
388 void *Mem_pool::mp_new_block(size_t size)
389 {
390     int ret;
391     uchar *block = nullptr;
392     uchar *addr = nullptr;
393     Mem_node *node = nullptr;
394     Mem_node *current = m_current;
395     Mem_node *cur = nullptr;
396  
397     //int ret = posix_memalign((void **)&block, MP_ALIGNMENT, PAGE_SIZE);
398     //block = (uchar *)malloc(PAGE_SIZE);
399     ret = posix_memalign((void **)&block, MP_ALIGNMENT, PAGE_SIZE);
400     if (ret != 0)
401     {
402         cout << "malloc error" << endl;
403         return nullptr;
404     }
405  
406     node = (Mem_node *)block;
407     node->init();
408  
409     addr = (uchar *)mp_align_ptr(block + sizeof(Mem_node), MP_ALIGNMENT);
410     node->node_malloc(size);
411  
412     for (cur = current; cur->get_next() != nullptr; cur = cur->get_next())
413     {
414         if (cur->get_failed() > 4)
415         {
416             current = cur;
417         }
418     }
419  
420     cur->set_next(node);
421     m_current = current;
422     return addr;
423 }
424  
425 //向堆空间申请一块长度大于标准块长度的自定义大块空间
426 void *Mem_pool::mp_new_large(size_t size)
427 {
428     uchar *big_addr = nullptr;
429     Mem_large *large = nullptr;
430     int ret;
431     int n = 0;
432  
433     ret = posix_memalign((void **)&big_addr, MP_ALIGNMENT, size);
434     if (ret)
435     {
436         return nullptr;
437     }
438  
439     large = m_large;
440     for (large; large != nullptr; large = large->get_next())
441     {
442         if (large->get_alloc() == nullptr)
443         {
444             large->large_reset(size, big_addr);
445             return big_addr;
446         }
447         if (n++ > 3) //防止过多次的遍历,限制次数
448         {
449             break;
450         }
451     }
452  
453     large = (Mem_large *)mp_new(sizeof(Mem_large));
454  
455     if (large == nullptr)
456     {
457         free(big_addr);
458         return nullptr;
459     }
460  
461     large->large_reset(size, big_addr);
462     //头插法
463     large->set_next(m_large);
464     m_large = large;
465  
466     return big_addr;
Mymemory.cpp