转 内存池技术的原理与实现

内存池技术的原理与实现

序言

  最近在网上看到了几篇篇讲述内存池技术的文章,有一篇是有IBM中国研发中心的人写的,写的不错~~文章地址在本篇blog最后。原文的讲述比我的要清晰很多,我在这只是把我的一些理解和遇到的一些问题和大家分享一下~~

一、为什么要使用内存池技术呢

  主要有两个原因:1、减少new、delete次数,减少运行时间;2、避免内存碎片。

  1、效率

  c语言中使用malloc/free来分配内存,c++中使用new/delete来分配内存,他们的内存申请与释放都是与操作系统进行交互的。具体的内容在严蔚敏数据结构的第八章有相关讲述,主要就是系统要维护一个内存链表,当有一个内存申请过来时,根据相应的分配算法在链表中找个一个合适的内存分配给它。这些算法有的是分配最先找到的不小于申请内存的内存块,有的是分配最大的内存块,有的是分配最接近申请内存大小的内存块。分配的内存块可能会大于所申请的内存大小,这样还有进行切割,将剩余的内存插入到空闲链表中。当释放的时候,系统可能要对内存进行整理,判断free的内存块的前后是否有空闲,若有的话还要进行合并。此外,new/delete还要考虑多线程的情况。总之一句话,调用库中的内存分配函数,十分的耗时~~

  2、内存碎片

  什么是内存碎片内,从字面意思就很好理解了,就是内存不再是一整块的了,而是碎了。因为连续的这种new/delete操作,一大块内存肯能就被分割成小的内存分配出去了,这些小的内存都是不连续的。当你再去分配大的连续内存的时候,尽管剩余内存的总和可能大于所要分配的内存大小,但系统就找不到连续的内存了,所以导致分配错误。malloc的时候会导致返回NULL,而new的时候再vc6.0中返回NULL,vs2003以上则是抛出异常。

二、原理

  要解决上述两个问题,最好的方法就是内存池技术。具体方法就是大小固定、提前申请、重复利用。

  因为内存的申请和释放是很低效的,所以我们只在开始时申请一块大的内存(在该块内存不够用时在二次分配),然后每次需要时都从这块内存中取出,并标记下这块内存被用了,释放时标记此内存被释放了。释放时,并不真的把内存释放给操作系统,只要在一大块内存都空闲的时候,才释放给操作系统。这样,就减少了new/delete的操作次数,从而提高了效率。

  在调用内存分配函数的时候,大部分时间所分配的内存大小都是一定的,所以可以采用每次都分配固定大小的内存块,这样就避免了内存碎片产生的可能。

三、具体实现

  我所采用的内存池的构造方法完全是按照文章1所介绍的方法,内存池的结构图如下:

  

  如图所示MemoryPool是一个内存池类,其中pBlock是一个指向了一个内存块的指针,nUintSzie是分配单元的大小,nInitSize是第一次分配时向系统申请的内存的大小,nGrouSize是后面每次向系统申请的内存的大小。

  MemoryBloc代表一个内存块单元,它有两部分构成,一部分时MemoryBlock类的大小,另一部分则是实际的内存部分。一个MemoryBlock的内存是在重载的new操作符中分配的,如下所示: 

void* MemoryBlock::operator new(size_t, int nUnitSize,int nUnitAmount )

{

    return ::operator new( sizeof(MemoryBlock) + nUnitSize * nUnitAmount );

}

    MemoryBlock内中,nSize代码该内存块的大小(系统分配内存大小-MemoryBlock类的大小),nFree是空闲内存单元的个数,nFirst代表的是下一个要分配的内存单元的序号。aData是用来记录待分配内存的位置的。因为要分配的内存是在new中一起向系统申请的,并没有一个指针指向这块内存的位置,但它的位置就在MemoryBlock这个类的地址开始的,所以可以用MemoryBlock的最后一个成员的位置来表示待分配内存的位置。

  带分配内存中,是以nUnitSize为单位的,一个内存单元的头两个字节都记录了下一个要分配的内存单元的序号,序号从0开始。这样实际也就构成了一个数组链表。由MemoryBlock的构造函数来完成这个链表的初始化工作:

MemoryBlock::MemoryBlock( int nUnitSize,int nUnitAmount )

    :   nSize   (nUnitAmount * nUnitSize),

        nFree   (nUnitAmount - 1),  //构造的时候,就已将第一个单元分配出去了,所以减一

        nFirst  (1),                //同上

        pNext   (NULL)

{

    //初始化数组链表,将每个分配单元的下一个分配单元的序号写在当前单元的前两个字节中

    char* pData = aData;

    //最后一个位置不用写入

    for( int i = 1; i < nSize - 1; i++)

    {

        (*(USHORT*)pData) = i;

        pData += nUnitSize;

    }

}

  

  在MemoryPool的Alloc()中,遍历block链表,找到nFree大于0的block,从其上分配内存单元。然后将nFree减一,修改nFirst的值。

  在MemoryPool的Free(pFree)函数中,根据pFree的值,找到它所在的内存块,然后将它的序号作为nFirst的值(因为它绝对是空闲的),在pFree的头两个字节中写入原来nFirst的值。然后要判断,该block是否全部为free,方法是检测nFree * nUnitSize == nSize。若是,则向系统释放内存,若不是,则将该block放到链表的头部,因为该block上一定含有空隙的内存单元,这样可以减少分配时遍历链表所消耗的时间。

四、使用

  内存池一般都是作为一个类的静态成员,或者全局变量。使用时,重载new操作符,使其到MemoryPool中去分配内存,而不是向系统申请。这样,一个类的所以对象都在一个内存池中开辟空间。

void CTest::operator delete( void* pTest )

{  

    Pool.Free(pTest);  

}

void* CTest::operator new(size_t)

{

    return (CTest*)Pool.Alloc();

}

   

五、代码

MemoryPool.h


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

#include <stdlib.h>

#include <wtypes.h>

#define  MEMPOOL_ALIGNMENT 8            //对齐长度

//内存块,每个内存块管理一大块内存,包括许多分配单元

class MemoryBlock

{

public:

                    MemoryBlock (int nUnitSize,int nUnitAmount);

                    ~MemoryBlock(){};

    static void*    operator new    (size_t,int nUnitSize,int nUnitAmount);

    static void     operator delete (void* ,int nUnitSize,int nUnitAmount){};

    static void     operator delete (void* pBlock);

    int             nSize;              //该内存块的大小,以字节为单位

    int             nFree;              //该内存块还有多少可分配的单元

    int             nFirst;             //当前可用单元的序号,从0开始

    MemoryBlock*    pNext;              //指向下一个内存块

    char            aData[1];           //用于标记分配单元开始的位置,分配单元从aData的位置开始

    

};

class MemoryPool

{

public:

                    MemoryPool (int _nUnitSize,

                                int _nGrowSize = 1024,

                                int _nInitSzie = 256);

                    ~MemoryPool();

    void*           Alloc();

    void            Free(void* pFree);

private:

    int             nInitSize;          //初始大小

    int             nGrowSize;          //增长大小

    int             nUnitSize;          //分配单元大小

    MemoryBlock*    pBlock;             //内存块链表

};

 MemoryPool.cpp


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

#include "MemoryPool.h"

MemoryBlock::MemoryBlock( int nUnitSize,int nUnitAmount )

    :   nSize   (nUnitAmount * nUnitSize),

        nFree   (nUnitAmount - 1),  //构造的时候,就已将第一个单元分配出去了,所以减一

        nFirst  (1),                //同上

        pNext   (NULL)

{

    //初始化数组链表,将每个分配单元的下一个分配单元的序号写在当前单元的前两个字节中

    char* pData = aData;

    //最后一个位置不用写入

    forint i = 1; i < nSize - 1; i++)

    {

        (*(USHORT*)pData) = i;

        pData += nUnitSize;

    }

}

void* MemoryBlock::operator new(size_tint nUnitSize,int nUnitAmount )

{

    return ::operator newsizeof(MemoryBlock) + nUnitSize * nUnitAmount );

}

void MemoryBlock::operator deletevoid* pBlock)

{

    ::operator delete(pBlock);

}

MemoryPool::MemoryPool( int _nUnitSize, int _nGrowSize /*= 1024*/int _nInitSzie /*= 256*/ )

{

    nInitSize = _nInitSzie;

    nGrowSize = _nGrowSize;

    pBlock = NULL;

    if(_nUnitSize > 4)

        nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT - 1)) & ~(MEMPOOL_ALIGNMENT - 1);

    else if( _nUnitSize < 2)

        nUnitSize = 2;

    else

        nUnitSize = 4;

}

MemoryPool::~MemoryPool()

{

    MemoryBlock* pMyBlock = pBlock;

    while( pMyBlock != NULL)

    {

        pMyBlock = pMyBlock->pNext;

        delete(pMyBlock);

    }

}

void* MemoryPool::Alloc()

{

    if( NULL == pBlock)

    {

        //首次生成MemoryBlock,new带参数,new了一个MemoryBlock类

        pBlock = (MemoryBlock*)new(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);

        return (void*)pBlock->aData;

    }

    //找到符合条件的内存块

    MemoryBlock* pMyBlock = pBlock;

    while( pMyBlock != NULL && 0 == pMyBlock->nFree )

        pMyBlock = pMyBlock->pNext;

    if( pMyBlock != NULL)

    {

        //找到了,进行分配

        char* pFree = pMyBlock->aData + pMyBlock->nFirst * nUnitSize;

        pMyBlock->nFirst = *((USHORT*)pFree);

        pMyBlock->nFree--;

        return (void*)pFree;

    }

    else

    {

        //没有找到,说明原来的内存块都满了,要再次分配

        if( 0 == nGrowSize)

            return NULL;

        

        pMyBlock = (MemoryBlock*)new(nUnitSize,nGrowSize) MemoryBlock(nUnitSize,nGrowSize);

        if( NULL == pMyBlock)

            return NULL;

        //进行一次插入操作

        pMyBlock->pNext = pBlock;

        pBlock = pMyBlock;

        return (void*)pMyBlock->aData;

    }

}

void MemoryPool::Free( void* pFree )

{

    //找到p所在的内存块

    MemoryBlock* pMyBlock = pBlock;

    MemoryBlock* PreBlock = NULL;

    while ( pMyBlock != NULL && ( pBlock->aData > pFree || pMyBlock->aData + pMyBlock->nSize))

    {

        PreBlock = pMyBlock;

        pMyBlock = pMyBlock->pNext;

    }

    if( NULL != pMyBlock )      //该内存在本内存池中pMyBlock所指向的内存块中

    {      

        //Step1 修改数组链表

        *((USHORT*)pFree) = pMyBlock->nFirst;

        pMyBlock->nFirst  = (USHORT)((ULONG)pFree - (ULONG)pMyBlock->aData) / nUnitSize;

        pMyBlock->nFree++;

        //Step2 判断是否需要向OS释放内存

        if( pMyBlock->nSize == pMyBlock->nFree * nUnitSize )

        {

            //在链表中删除该block

            

            delete(pMyBlock);

        }

        else

        {

            //将该block插入到队首

            PreBlock = pMyBlock->pNext;

            pMyBlock->pNext = pBlock;

            pBlock = pMyBlock;

        }

    }

}

 CTest.cpp


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

#include <stdio.h>

#include "MemoryPool.h"

class CTest

{

public:

                CTest(){data1 = data2 = 0;};

                ~CTest(){};

    void*       operator new (size_t);

    void        operator delete(void* pTest);

public:

    static      MemoryPool Pool;

    int         data1;

    int         data2;

};

void CTest::operator deletevoid* pTest )

{  

    Pool.Free(pTest);  

}

void* CTest::operator new(size_t)

{

    return (CTest*)Pool.Alloc();

}

MemoryPool CTest::Pool(sizeof(CTest));

int main()

{

    CTest* pTest = new CTest;

    printf("%d",pTest->data2);

}

六、问题

  在编写代码时,遇到了一些小问题,现与大家分享如下:

  1、重载new操作符时,编译器要求是第一个参数必须是size_t,返回值必须是void*;free的第一个参数必须是void*.

  2、一般要在类的成员中重载new操作符,而不要重载全局的new操作符。

  3、一个类中要是重载了一个new操作符,一定要有一个相应类型的delete操作符,可以什么都不干,但必须有,否则在构造函数失败时,找不到对应的delete函数。

例如:  


1

2

static void*    operator new    (size_t,int nUnitSize,int nUnitAmount);

    static void     operator delete (void* ,int nUnitSize,int nUnitAmount){};

  4、带参数的new操作符

pBlock = (MemoryBlock*)new(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);

  第一个nUnitSize nInitSize是new操作符的参数,该new操作符是new了一个MemoryBlock对象,在new返回的地址上构造MemoryBlock的对象。

  5、如果在类的内部不能进行静态成员的定义的话,可以只在内部进行声明,在外部定义:

MemoryPool CTest::Pool(sizeof(CTest));

  

------------------------------------------------------------END----------------------------------------------------------------------

文章1:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html

文章2:http://www.codeproject.com/Articles/27487/Why-to-use-memory-pool-and-how-to-implement-it

时间: 2024-12-07 07:03:26

转 内存池技术的原理与实现的相关文章

内存池技术的原理与实现

6.1 自定义内存池性能优化的原理 如前所述,读者已经了解到"堆"和"栈"的区别.而在编程实践中,不可避免地要大量用到堆上的内存.例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从内存堆上分配或者释放一定的内存:在维护一个动态数组时,如果动态数组的大小不能满足程序需要时,也要在内存堆上分配新的内存空间. 6.1.1 默认内存管理函数的不足 利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的

Netty精粹之轻量级内存池技术实现原理与应用

摘要: 在Netty中,通常会有多个IO线程独立工作,基于NioEventLoop的实现,每个IO线程负责轮询单独的Selector实例来检索IO事件,当IO事件来临的时候,IO线程开始处理IO事件.最常见的IO事件即读写事件,那么这个时候就会涉及到IO线程对数据的读写问题,具体到NIO方面即从内核缓冲区读取数据到用户缓冲区或者从用户缓冲区将数据写到内核缓冲区.NIO提供了两种Buffer作为缓冲区,即DirectBuffer和HeapBuffer.这篇文章主要在介绍两种缓冲区的基础之上再介绍N

内存池技术介绍(图文并茂,非常清楚)

看到一篇关于内存池技术的介绍文章,受益匪浅,转贴至此. 原贴地址:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html 6.1 自定义内存池性能优化的原理 如前所述,读者已经了解到"堆"和"栈"的区别.而在编程实践中,不可避免地要大量用到堆上的内存.例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从内存堆上分配或者释放一定的内存:在维护一个动态数组时,如果动态数组的

boost::pool与内存池技术

建议看这个链接的内容:http://cpp.winxgui.com/cn:mempool-example-boost-pool Pool分配是一种分配内存方法,用于快速分配同样大小的内存块,    尤其是反复分配/释放同样大小的内存块的情况. 1. pool 快速分配小块内存,如果pool无法提供小块内存给用户,返回0. Example: void func()    {      boost::pool<> p(sizeof(int));                      ^^^^^

极高效内存池实现 (cpu-cache)

1.内存池的目的 提高程序的效率 减少运行时间 避免内存碎片 2.原理 要解决上述两个问题,最好的方法就是内存池技术.具体方法就是,申请内存 :大小固定,提前申请,重复利用. 3.使用场合 长时间运行的服务程序 对速度要求高的程序 对稳定性要求高的程序 4.内存池不能满足所有的需求 内存池是不能够满足所有人的需求的,那么考虑到通用性,健壮性,需要考虑到,当申请内存块 大小不在内存池 中的情况 或者内存池中已经没有内存块了,所采取的措施:直接使用系统的申请,释放函数. 内存池的设计 : 内存池中有

Linux简易APR内存池学习笔记(带源码和实例)

先给个内存池的实现代码,里面带有个应用小例子和画的流程图,方便了解运行原理,代码 GCC 编译可用.可以自己上网下APR源码,参考代码下载链接: http://pan.baidu.com/s/1hq6A20G 贴两个之前学习的时候参考的文章地址,大家可以参考: http://www.cnblogs.com/bangerlee/archive/2011/09/01/2161437.html http://blog.csdn.net/flyingfalcon/article/details/2627

高性能之内存池

内存池(Memory Pool)是一种内存分配方式. 通常我们习惯直接使用new.malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能.内存池则是在真正使用内存之前,先申请分配一定数量的.大小相等(一般情况下)的内存块留作备用.当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存.这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升. (1)针对特殊情况,例如需要频繁分配释放固定大

详谈内存管理技术(二)、内存池

嗯,这篇讲可用的多线程内存池. 零.上期彩蛋:不要重载全局new 或许,是一次很不愉快的经历,所以在会有这么一个"认识".反正,大概就是:即使你足够聪明,也不要自作聪明:在这就是不要重载全局new,无论你有着怎样的目的和智商.因为: class XXX{ public: XXX* createInstance(); }; 这是一个不对称的接口:只告诉了我们如何创建一个[堆]对象,但是释放呢??! 很无奈,只能假设其使用全局默认的delete来删除(除此之外,没有其他选择):这时,我为了

数据库连接池的工作原理以及这项技术的产生

为什么要有这项技术? 数据库连接是一种非常珍贵而且有限的资源,尤其是在多用户的网络上,对数据库的管理好坏直接影响整个系统的性能 一.建立一个数据库连接是一项非常耗时的操作,在页面应用中如果每次用户都需要创建一次数据库连接,那么响应的时间就会非常长,会影响用户体验 二.数据库连接数是有限的,如果管理不好用户经常与数据库建立连接却忘记了释放,那么运行时间久了,数据库连接资源将会被耗尽,当再有新的用户操作时将会进行等待,直到资源被释放,这对系统的可用性造成了影响.因此管理好数据库连接资源非常重要,尤其