Box2d源码学习<二>内存管理之SOA的实现

本系列博客是由扭曲45原创,欢迎转载,转载时注明出处,http://blog.csdn.net/cg0206/article/details/8258166

SOA,全称small object allocator,中文意思是小对象分配器。box2d虽然是用c++写的,但是并没有使用c++自带的new/delete实现内存管理,而是使用在c的malloc/free做法的基础上封装了类b2BlockAllocator进行内存管理,使得分配和使内存变得更加高效、快速。其中b2BlockAllocator就是一个SOA,下面我们就对源码进行分析。


一、b2BlockAllocator类的头文件

首先我们对头文件b2BlockAllocator.h进行大致的了解一遍。不多说,上代码:

[cpp]  view plain copy
  1. <span style="font-size:14px;">//一次分配内存大小  
  2. const int32 b2_chunkSize = 16 * 1024;  
  3. //块子节点大小的最大值  
  4. const int32 b2_maxBlockSize = 640;  
  5. //可以申请块子节点大小的类型总数  
  6. const int32 b2_blockSizes = 14;  
  7. //块空间增量  
  8. const int32 b2_chunkArrayIncrement = 128;  
  9. //块子节点结构体[链表实现]声明  
  10. struct b2Block;  
  11. //块结构体声明  
  12. struct b2Chunk;  
  13.   
  14. //这是一个小型的对象分配器,用于一次分配多个小对象  
  15. class b2BlockAllocator  
  16. {  
  17. public:  
  18.     b2BlockAllocator();  
  19.     ~b2BlockAllocator();  
  20.   
  21.     //分配内存,当size>b2_maxBlockSize则直接用b2Alloc分配  
  22.     void* Allocate(int32 size);  
  23.   
  24.     //释放内存,当size>b2_maxBlockSize则直接用b2Free释放  
  25.     void Free(void* p, int32 size);  
  26.     //清空内存  
  27.     void Clear();  
  28.   
  29. private:  
  30.     //当前块的头指针  
  31.     b2Chunk* m_chunks;  
  32.     //当前已使用的块空间节点总数  
  33.     int32 m_chunkCount;  
  34.     //当前已申请的块空间节点总数  
  35.     int32 m_chunkSpace;  
  36.     //未被使用的内存块链表类型数组,保存了其不同类型链表的头指针  
  37.     b2Block* m_freeLists[b2_blockSizes];  
  38.     //申请的块大小类型数组  
  39.     static int32 s_blockSizes[b2_blockSizes];  
  40.     //根据要申请块的大小获取其类型索引的数组  
  41.     static uint8 s_blockSizeLookup[b2_maxBlockSize + 1];  
  42.     //是否已初始化s_blockSizeLookup数组,标志变量  
  43.     static bool s_blockSizeLookupInitialized;  
  44. };  
  45. </span>  

上面文字是对相关字段及方法的注释,我们就不对其进行讲解了。


二、b2BlockAllocator的.c文件

下面我们看该类的具体实现,看b2BlockAllocator.c文件,
1、变量的定义

映入我们眼帘的是一些变量或结构的定义,如下代码:

[cpp]  view plain copy
  1. <span style="font-size:14px;">int32 b2BlockAllocator::s_blockSizes[b2_blockSizes] =   
  2. {  
  3.     16,        // 0  
  4.     32,        // 1  
  5.     64,        // 2  
  6.     96,        // 3  
  7.     128,    // 4  
  8.     160,    // 5  
  9.     192,    // 6  
  10.     224,    // 7  
  11.     256,    // 8  
  12.     320,    // 9  
  13.     384,    // 10  
  14.     448,    // 11  
  15.     512,    // 12  
  16.     640,    // 13  
  17. };  
  18. uint8 b2BlockAllocator::s_blockSizeLookup[b2_maxBlockSize + 1];  
  19. bool b2BlockAllocator::s_blockSizeLookupInitialized;  
  20.   
  21. struct b2Chunk  
  22. {  
  23.     int32 blockSize;  
  24.     b2Block* blocks;  
  25. };  
  26.   
  27. struct b2Block  
  28. {  
  29.     b2Block* next;  
  30. };</span>  
  31. s_blockSizes       :是申请的块子节点大小类型数组,主要负责将相应大小的子节点分类;
    blockSizeLookup:是根据要申请的子节点size获取s_blockSizes数组的索引,并保持到该数组中;
    s_blockSizeLookupInitialized:是否已初始化s_blockSizeLookup数组,标志变量,用于只需要初始化很少次数的变量,可以人为控制【但是最好还是不要那么做】,默认值是false,也许大家感到奇怪,这个变量在哪初始化的,大家可以猜猜看。
    b2Chunk是块结构体,其中blockSize表示块子节点大小,blocks表示块头指针;
    b2Block表示块子节点结构体,next表示下一个块头指针,如果你感觉到很熟悉的话,那就对了,这是典型的链表定义,将会用链表将子节点链接起来。


    2、函数的实现
        1)、构造函数和析构函数
    接下来就是该类的构造函数和析构函数了,同样我们也看代码。

    [cpp]  view plain copy
    1. <span style="font-size:14px;">b2BlockAllocator::b2BlockAllocator()  
    2. {  
    3.     b2Assert(b2_blockSizes < UCHAR_MAX);  
    4.   
    5.     m_chunkSpace = b2_chunkArrayIncrement;  
    6.     m_chunkCount = 0;  
    7.     m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));  
    8.       
    9.     memset(m_chunks, 0, m_chunkSpace * sizeof(b2Chunk));  
    10.     memset(m_freeLists, 0, sizeof(m_freeLists));  
    11.   
    12.     if (s_blockSizeLookupInitialized == false)  
    13.     {  
    14.         int32 j = 0;  
    15.         for (int32 i = 1; i <= b2_maxBlockSize; ++i)  
    16.         {  
    17.             b2Assert(j < b2_blockSizes);  
    18.             if (i <= s_blockSizes[j])  
    19.             {  
    20.                 s_blockSizeLookup[i] = (uint8)j;  
    21.             }  
    22.             else  
    23.             {  
    24.                 ++j;  
    25.                 s_blockSizeLookup[i] = (uint8)j;  
    26.             }  
    27.         }  
    28.   
    29.         s_blockSizeLookupInitialized = true;  
    30.     }  
    31. }  
    32.   
    33. b2BlockAllocator::~b2BlockAllocator()  
    34. {  
    35.     for (int32 i = 0; i < m_chunkCount; ++i)  
    36.     {  
    37.         b2Free(m_chunks[i].blocks);  
    38.     }  
    39.   
    40.     b2Free(m_chunks);  
    41. }</span>  
    在构造函数b2BlockAllocator()中我们初始化相关变量,例如一开始我们就判断b2_blockSizes的有效性,接着为m_chunkSpace、m_chunkCount、m_chuns、m_freeLists、和s_blockSizeLookup的初始化。我们主要说说s_blockSizeLookup的初始化是怎样完成的。
    a)、用s_blockSizeLookupInitialized判断s_blockSizeLookup是否已初始化,若没有则进入。大家猜到绿色部分的疑问了没,如果你的答案是编译器,那就恭喜你了。
    b)、里面的for循环主要是根据块的大小,将块分类成以上14中类型,并设置索引值,保存到s_blockSizeLookup数组中,j保存的是s_blockSizes数组的索引值。
    c)、接着判断j的有效性,这里用的是assert断言,关于assert断言的又不懂的童鞋可以参照维基百科上面的解释http://zh.wikipedia.org/wiki/Assert.h
    d)、if/else中i不大于j索引对应的块大小类型的数组,则将j索引的值赋给类型索引数组,例如,j = 0时,i的值可以是 1-16。否则i>j索引对应的块大小类型的数组,则将j索引自增,赋值。上面代码可以简化成:

    [cpp]  view plain copy
    1. <span style="font-size:14px;">if(i > s_blockSizes[j])  
    2. {  
    3.     ++j;  
    4. }  
    5. s_blockSizeLookup[i] = (uint8)j;</span>  
    虽然效率差了点:-D,但这样更易于理解。
    e)、将标志变量置s_blockSizeLookupInitialized为true,表示已经初始化在析构函数~b2BlockAllocator()我们释放了当前块的每个子节点,和整个块。


    2)、内存管理函数内存管理函数分为三个:Allocate分配函数;Free释放函数;Clear清理内存函数
    关于Allocate函数,代码如下:
    [cpp]  view plain copy
    1. <span style="font-size:14px;">void* b2BlockAllocator::Allocate(int32 size)  
    2. {  
    3.     if (size == 0)  
    4.         return NULL;  
    5.     //验证size的有效性  
    6.     b2Assert(0 < size);  
    7.     //申请的空间大于规定的最大值,  
    8.     //直接申请,不放到块的链表中去【即m_chunks】  
    9.     if (size > b2_maxBlockSize)  
    10.     {  
    11.         return b2Alloc(size);  
    12.     }  
    13.     //根据要申请的内存大小获取内存类型索引值,并判断有效性  
    14.     int32 index = s_blockSizeLookup[size];  
    15.     b2Assert(0 <= index && index < b2_blockSizes);  
    16.     //查看是否有同类型的未被使用的内存块  
    17.     if (m_freeLists[index])  
    18.     {  
    19.         b2Block* block = m_freeLists[index];  
    20.         m_freeLists[index] = block->next;  
    21.         return block;  
    22.     }  
    23.     else  
    24.     {  
    25.         //已使用的大小与已申请的块大小相等  
    26.         //重新申请空间  
    27.         if (m_chunkCount == m_chunkSpace)  
    28.         {  
    29.             //获取原来的块头,并保存到oldChunks中  
    30.             b2Chunk* oldChunks = m_chunks;  
    31.             //扩充块空间的大小  
    32.             m_chunkSpace += b2_chunkArrayIncrement;  
    33.             //申请空间,并重新赋值给m_chunks变量  
    34.             m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));  
    35.             //拷贝内存到m_chunks中  
    36.             memcpy(m_chunks, oldChunks, m_chunkCount * sizeof(b2Chunk));  
    37.             //将最新申请的内存的最后b2_chunkArrayIncrement置,防止程序中读取脏数据  
    38.             //个人感觉如下写法更易于理解,只是效率要慢一点点啦  
    39.             ///memset(m_chunks , 0,m_chunkSpace * sizeof(b2Chunk));  
    40.             ///memcpy(m_chunks, oldChunks, m_chunkCount * sizeof(b2Chunk));  
    41.             memset(m_chunks + m_chunkCount, 0, b2_chunkArrayIncrement * sizeof(b2Chunk));  
    42.             //释放原来的块空间  
    43.             b2Free(oldChunks);  
    44.         }  
    45.         //获取已使用块空间的尾指针  
    46.         b2Chunk* chunk = m_chunks + m_chunkCount;  
    47.         //申请n个块子节点内存  
    48.         //并将地址赋值给块头指针  
    49.         //这样的好处是不需要频繁的去内存中申请空间,不必每个节点都去申请,提高了效率  
    50.         chunk->blocks = (b2Block*)b2Alloc(b2_chunkSize);  
    51.                 //用于调试,正式版本中将关闭_DEBUG宏,故不存在相关代码,以后我们遇到相关代码块也将忽略  
    52. #if defined(_DEBUG)</span>  
    53. memset(chunk->blocks, 0xcd, b2_chunkSize);  
    54. #endif  
    55.         //获取根据索引块大小,并赋值给块的大小  
    56.         int32 blockSize = s_blockSizes[index];  
    57.         chunk->blockSize = blockSize;  
    58.         //获取子节点个数  
    59.         //并防止转换时出现错误【个人猜测,有待考证】  
    60.         int32 blockCount = b2_chunkSize / blockSize;  
    61.         b2Assert(blockCount * blockSize <= b2_chunkSize);  
    62.         //将子节点用链表的方式串起来  
    63.         //有人不禁疑惑,这不是刚刚申请的一个连续的内存块吗?  
    64.         //用头指针可以直接访问呀?干嘛要串起来?  
    65.         //好处:  
    66.         //可以自由的操作每个子节点,例如、释放、访问、其它方式链接等,在后面我们将看见  
    67.         for (int32 i = 0; i < blockCount - 1; ++i)  
    68.         {  
    69.             b2Block* block = (b2Block*)((int8*)chunk->blocks + blockSize * i);  
    70.             b2Block* next = (b2Block*)((int8*)chunk->blocks + blockSize * (i + 1));  
    71.             block->next = next;  
    72.         }  
    73.         //获取最后一个内存块的子节点  
    74.         //并将子节点的下一个节点置空  
    75.         b2Block* last = (b2Block*)((int8*)chunk->blocks + blockSize * (blockCount - 1));  
    76.         last->next = NULL;  
    77.         //将申请且未使用的指针保存到m_freeLists对应类型的数组中  
    78.         m_freeLists[index] = chunk->blocks->next;  
    79.         //当前已使用的块空间节点总数  
    80.         ++m_chunkCount;  
    81.         //返回块的头指针  
    82.         return chunk->blocks;  
    83.     }  

    代码的解释如上述,在此就不啰嗦了,不过我们总体分析一下,它的逻辑是:

    a)、将内存按大小分为16,32,64,96,128,160,192,224,256...640等b2_blockSizes【即14】类,并按照顺序保存到数组s_blockSizes中。

    b)、通过申请size的大小,判断是否大于b2_maxBlockSize,如果大于则直接分配。

    c)、否则通过size,传递给s_blockSizeLookup数组找到所需要申请的类型index,将index的值在传递给链表数组m_freelists[index],查找是否有子节点,有则直接返回子节点。

    d)、否则将判断m_chunks指向的动态数组是否已用完,若用完则扩充块空间大小加b2_chunkArrayIncrement,重新申请空间,并将空间的内存拷贝到现在的空间中,并释放原内存空间。

    e)、通过m_chunks+m_chunkCount获取块空间的m_chunks动态数组的未被使用的空间元素,申请大小为b2_chunkSize的内存,并将其分成对应类型的n块空间,并将这n个子节点串链起来,形成链表。将链表的下一个指针保存到对应类型的m_freeLists数组中,同时返回头指针作为申请的内存地址。

    关于Free函数,代码和注释如下:

    [cpp]  view plain copy
    1. <span style="font-size:14px;">  
    2. void b2BlockAllocator::Free(void* p, int32 size)  
    3. {  
    4.     //判断检测size是否有效  
    5.     //并作相应的处理  
    6.     if (size == 0)  
    7.     {  
    8.         return;  
    9.     }  
    10.   
    11.     b2Assert(0 < size);  
    12.   
    13.     if (size > b2_maxBlockSize)  
    14.     {  
    15.         b2Free(p);  
    16.         return;  
    17.     }  
    18.     //根据内存大小获取索引值,并判断是否有效  
    19.     int32 index = s_blockSizeLookup[size];  
    20.     b2Assert(0 <= index && index < b2_blockSizes);  
    21.   
    22. #ifdef _DEBUG  
    23.     // Verify the memory address and size is valid.  
    24.     int32 blockSize = s_blockSizes[index];  
    25.     bool found = false;  
    26.     for (int32 i = 0; i < m_chunkCount; ++i)  
    27.     {  
    28.         b2Chunk* chunk = m_chunks + i;  
    29.         if (chunk->blockSize != blockSize)  
    30.         {  
    31.             b2Assert(   (int8*)p + blockSize <= (int8*)chunk->blocks ||  
    32.                         (int8*)chunk->blocks + b2_chunkSize <= (int8*)p);  
    33.         }  
    34.         else  
    35.         {  
    36.             if ((int8*)chunk->blocks <= (int8*)p && (int8*)p + blockSize <= (int8*)chunk->blocks + b2_chunkSize)  
    37.             {  
    38.                 found = true;  
    39.             }  
    40.         }  
    41.     }  
    42.   
    43.     b2Assert(found);  
    44.   
    45.     memset(p, 0xfd, blockSize);  
    46. #endif  
    47.     //获取块的块的头指针并插入到相应的空闲链表的头部【注意是子节点从链表头部插入】,并保存相应的头指针到m_freeLists中去  
    48.     b2Block* block = (b2Block*)p;  
    49.     block->next = m_freeLists[index];  
    50.     m_freeLists[index] = block;  
    51. }</span>  

    同样,_DEBUG宏类的我们不做讨论。这里说明一下,若内存小于等于b2_maxBlockSize时,此时内存空间并没有释放,而是将链接到相应类型的空闲链表中,并且是从链表头部插入此节点的,对此这也是很容易做到的,这也是刚刚申请空间将连续的空间分割,再次链接成链表的原因。


    再看Clear函数
    [cpp]  view plain copy
    1. <span style="font-size:14px;">void b2BlockAllocator::Clear()  
    2. {  
    3.     //释放当前已使用的块空间大小  
    4.     for (int32 i = 0; i < m_chunkCount; ++i)  
    5.     {  
    6.         b2Free(m_chunks[i].blocks);  
    7.     }  
    8.   
    9.     m_chunkCount = 0;  
    10.     //清空块  
    11.     memset(m_chunks, 0, m_chunkSpace * sizeof(b2Chunk));  
    12.     //清空未被使用的内存块链表类型数组  
    13.     memset(m_freeLists, 0, sizeof(m_freeLists));  
    14. }</span>  

    只是释放了形成链表的块内存,m_chunks和m_freeLists也只是清空其内容,真正释放它们是在上面说的类的析构函数中。

    ok,不多说了,有什么错误、不妥之处,希望大家能多多指正。也希望和大家多多交流。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值