Box2D源码学习(2)-b2BlockAllocator块内存分配

Box2D目录下主要包括四个文件夹,

Collision-碰撞相关代码

Common-通用代码,包含块内存分配,栈内存分配,计时器等。

Dynamics-Box2D世界,物体,形状等定义

Rope-绳连接的定义

还是一个一个攻破这些代码吧,先从通用代码common入手,其中负责内容分配的源码有以下四个文件。

b2BlockAllocator.cpp

b2BlockAllocator.h

b2StackAllocator.cpp

b2StackAllocator.h

本节分析块内存的分配。

块内存分配 

虽然C++有自己的内存分配new,或者c的malloc,但是由于物理引擎中需要分配大量的小型对象。并且生命周期很短,甚至只有几个时间步,使用系统自带的内存分配效率比较低。所以Box2D自己写了内存分配的代码。

Box2D使用的是被称为小型对象分配器(SOA:Small-Object Allocator)的东西来分配这些对象的内存。后面会分析SOA的源码。

有关SOA的介绍可以看这本书的第四章http://download.csdn.net/detail/chen52671/6391805。不过文字为繁体,看着不顺畅,有了SOA,在代码使用过程中就可以更灵活的为大量的小空间分配内存。

为了便于理解,简要解释一下其工作原理。SOA内存分配时,以分配30字节为例,是首先查看之前分配的内存还有没有空闲的,有的话,直接返回空闲内存地址以供使用,如果没有空闲内容,就要重新分配一块内存,分配内存时,会分配一大块内容(chunk),然后按照分配内存的大小,如本例中的30字节,分配一个够其使用的大小的block。大小为32字节。于是就将chunk这大块内存,分配为n个大小为32字节的block。并将第一个block地址返回,第二个block地址标记为free(空闲的)。以供以后再需要调用,以后如果再分配的内存是在16-32字节之间,就会来这里找free的内存并直接返回。

当不需要这块内存的时候,则调用Free()函数释放这小块内存,但是不是真正的释放,而是将其再次标记为空闲的,加入到空闲内存链表中,以供下次需要分配内存时,不需要真正的分配内存,而是直接将标记为空闲内存返回使用。

当真正需要释放掉这块内存是,调用Clear()来释放所有的chunks大区块的内存。

 

b2BlockAllocator.cppb2BlockAllocator.h主要定义了块内存的快速分配和回收机制。b2BlockAllocator.h中定义了class b2BlockAllocator类,除了构造函数外,只有Allocate(),Free(),Clear()分别用来分配内存,释放内存和真正的释放。当然头文件中还包括一些全局变量和一些私有数据,这里罗列一下,为下面的阅读提供方便,或者在阅读遇到问题时,回来翻阅。当把整篇文字读完后,应该就会知道这些数据的目的了。

全局常量:

const int32 b2_chunkSize = 16 * 1024;//chunk区块大小,chunk是一大块内存区域,内部可以分配若干小的block。Chunk大小为16K。

const int32 b2_maxBlockSize = 640;//Block最大640字节。

const int32 b2_blockSizes = 14;//block一共有14种大小,从16字节到640字节

const int32 b2_chunkArrayIncrement = 128;//初始分配128个chunk,当不够用的话,再增加128个重新分配,以此类推。

b2BlockAllocator类的私有成员:

b2Chunk* m_chunks;//这些chunks的首地址,

int32 m_chunkCount;//使用的chunk计数。

int32 m_chunkSpace;//目前分配的chunk的个数初始为128个。

b2Block* m_freeLists[b2_blockSizes];//标记为空闲的block的链表,一个14个链表,给不同大小block使用。

static int32 s_blockSizes[b2_blockSizes];//保存区块大小种类的数组,一共14种大小,分别为16,32,64...最大为640

static uint8 s_blockSizeLookup[b2_maxBlockSize + 1];//会在构造函数里初始化,分配0-16字节时,blockSizeLookup值为0,

//16-32时,该值为1.以此类推。是为了查找分配不同大小内存是,其位于第几个空闲链表中。

static bool s_blockSizeLookupInitialized;//标记上面那个数组是否已经被初始化了。

 

 

 

 

 

b2BlockAllocator构造函数

这部分具体代码参考源码吧。

首先,初始化这几个值,值得大小上面有说过。

m_chunkSpace 

m_chunkCount

m_chunks

其次,空闲区块列表m_freeLists初始化为空指针。

最后,保存区块大小种类的数组s_blockSizeLookup初始化。

Allocate(int32 size)

这部分没代码还说不清楚。那

Allocate函数主要作用是,根据参数size,分配内存,并返回内存地址。

1,如果size比预设的最大值640还大,那直接进行分配

if (size > b2_maxBlockSize)

{

return b2Alloc(size);//该函数调用malloc(size);直接分配。

}

2,根据要分配的内存的大小,确定其出在空闲链表数组的第几个,index可以理解为目录,一共14个目录,分别代表不同大小的blocks

 

index = s_blockSizeLookup[size];

3,如果空闲链表中有对应大小的区块,直接返回该空闲区块,并把空闲链表指针后移,指向下一块空闲区块

if (m_freeLists[index])

{

b2Block* block = m_freeLists[index];

m_freeLists[index] = block->next;

return block;

}

4,空闲链表没空闲的了,那就分配呗。分配的时候要先判断还有没有地方分配大内容块chunk?如果没有地方了,就增加m_chunks大小,原来是能容纳128chunks的话,现在可以容纳256个了。这个分配不是分配chunks的内存,不用担心浪费时间和空间,这个只是一堆指针。

if (m_chunkCount == m_chunkSpace)

{

b2Chunk* oldChunks = m_chunks;

m_chunkSpace += b2_chunkArrayIncrement;

m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));

memcpy(m_chunks, oldChunks, m_chunkCount * sizeof(b2Chunk));

memset(m_chunks + m_chunkCount, 0, b2_chunkArrayIncrement * sizeof(b2Chunk));

b2Free(oldChunks);

}

5,现在m_chunks有地方了,在下一个chunk,分配一个16k的空间以供使用。

 

b2Chunk* chunk = m_chunks + m_chunkCount;

chunk->blocks = (b2Block*)b2Alloc(b2_chunkSize);

6,在这16k的空间中分配n多个blocks。这里不是真的分配内存,因为内存上一步就分配了,这里是把内存分割,并把分割好的blocks首地址保存到链表。

int32 blockSize = s_blockSizes[index];

chunk->blockSize = blockSize;

int32 blockCount = b2_chunkSize / blockSize;

b2Assert(blockCount * blockSize <= b2_chunkSize);

for (int32 i = 0; i < blockCount - 1; ++i)

{

b2Block* block = (b2Block*)((int8*)chunk->blocks + blockSize * i);

b2Block* next = (b2Block*)((int8*)chunk->blocks + blockSize * (i + 1));

block->next = next;

}

b2Block* last = (b2Block*)((int8*)chunk->blocks + blockSize * (blockCount - 1));

last->next = NULL;

7,最后呢,就是返回切割好的blocks的指针,也就是首地址-第一块blocks啦。并把后面的block加入到对应目录的空闲链表里,比如分配30字节的空间,则会分成32字节的blockindex1,也就是空闲链表数组m_freeLists的第二个链表。不同大小区块不冲突。

 

m_freeLists[index] = chunk->blocks->next;

++m_chunkCount;

return chunk->blocks;

 

Free(void* p, int32 size)

其实free的过程和分配的过程是对应的。其会释放指针p指向的大小为size 的内存。

1,如果空间大于最大640字节,那直接调用b2Free来释放

if (size > b2_maxBlockSize)

{

b2Free(p);//会调用free(mem);来释放指针

return;

}

2,查找size这么大的内存,是放在哪个目录了

int32 index = s_blockSizeLookup[size];

b2Assert(0 <= index && index < b2_blockSizes);

3根据目录得到区块大小,如size30,则blockSize 32。然后需要验证p指针指向的这块地址是不是真的就是32字节大小。

int32 blockSize = s_blockSizes[index];

bool found = false;

for (int32 i = 0; i < m_chunkCount; ++i)

{

b2Chunk* chunk = m_chunks + i;

if (chunk->blockSize != blockSize)

{

b2Assert((int8*)p + blockSize <= (int8*)chunk->blocks ||

(int8*)chunk->blocks + b2_chunkSize <= (int8*)p);

}

else

{

if ((int8*)chunk->blocks <= (int8*)p && (int8*)p + blockSize <= (int8*)chunk->blocks + b2_chunkSize)

{

found = true;

}

}

}

 

b2Assert(found);

4,找到的话,把这个区块的内存都写为0xfd,(如果单就内存释放来说,这个没啥用,至于是否在其他地方有用,还没看),下一步就是把这块区块增加到第2个空闲列表数组m_freeLists[2]中,以供以后分配。

memset(p, 0xfd, blockSize);

b2Block* block = (b2Block*)p;

block->next = m_freeLists[index];

m_freeLists[index] = block;

Clear()

Clear()会彻底把那些内存释放掉,比如一共128chunk,那就分别那这些chunk都调用b2Free释放掉。并把指针指向0.

for (int32 i = 0; i < m_chunkCount; ++i)

{

b2Free(m_chunks[i].blocks);//调用free(mem);

}

 

m_chunkCount = 0;

memset(m_chunks, 0, m_chunkSpace * sizeof(b2Chunk));

memset(m_freeLists, 0, sizeof(m_freeLists));

析构函数

析构函数和Clear函数差不多

b2BlockAllocator::~b2BlockAllocator()

{

for (int32 i = 0; i < m_chunkCount; ++i)

{

b2Free(m_chunks[i].blocks);

}

 

b2Free(m_chunks);

}

不需要指针指向0是因为这些指针变量都不存在了,自然不会有野指针产生了。

使用实例

在上一篇文章的helloworld实例里,会调用CreateBody来创建物体。

b2Body* groundBody = world.CreateBody(&groundBodyDef); 

CreateBody实际就是用到了b2BlockAllocator。看看CreateBody的主要函数。

 

void* mem = m_blockAllocator.Allocate(sizeof(b2Body));

b2Body* b = new (mem) b2Body(def, this);

 

首先会调用b2BlockAllocatorAllocate方法,分配b2Body那么大的内存给它。

然后会使用new运算符来为创建一个b2Body从堆中创建一个对象,不过这个对象是在指定位置创建的。使用new (mem) b2Body(def, this);可以指定在mem的内存位置来创建对象。这种方式也被称为定位构造(placement new)。b2Body的构造函数中并没有从堆中分配内存。

通过这种方法,可以方便的分配小块内存,在对象不需要使用的时候,

b->~b2Body();

m_blockAllocator.Free(b, sizeof(b2Body));

调用对象的析构函数后(析构函数没有从堆中释放内存的过程),调用b2BlockAllocatorfree函数即可。轻松快捷。

你可能感兴趣的:(C++,box2D)