Box2D源码分析:小型对象分配器

本文分析Box2D中的小型对象分配器SOA,介绍如何通过维护不同尺寸的内存池来提高小对象分配和回收的效率。涉及chunk与block结构、内存分配与释放流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在某些场合下,我们会使用到非常小的对象(甚至可能只有数byte),其生命周期可能也很短,如果每次都通过malloc或者new在堆上分配内存,用后销毁,效率太低。于是维护一些不定尺寸并可扩展的内存池,当小对象需要内存分配时,直接从早已准备好的内存池上返回一块大小合适的内存。注意,当内存使用完毕后,我们需要返回其内存到内存池中,而不是销毁它。

本文以Box2D为例对SOA(smaller object allocator)进行分析。

首先对Box2Dchunk(大块内存)和block(区块)进行说明,如下图:


一个由多个16byte大小block组成的chunk,实际上就是一个链表。chunk是一个由nblock组成的固定大小的大块内存。而多个大块内存形成内存池。

首先看一下头文件

const int32 b2_chunkSize = 16 * 1024;        //一个chunk大小固定为16k
const int32 b2_maxBlockSize = 640;           //block最大的大小640b
const int32 b2_blockSizes = 14;              //block大小的种类,14种
const int32 b2_chunkArrayIncrement = 128;    //所有chunk用完时每次增加的chunk数量

struct b2Block;    //block结构,即链表节点结构
struct b2Chunk;    //chunk结构

class b2BlockAllocator
{
public:
	b2BlockAllocator();    //构造函数,对private成员变量进行初始化
	~b2BlockAllocator();   //析构函数,销毁内存

	void* Allocate(int32 size);        //在内存池中分配内存
	void Free(void* p, int32 size);    //只须返回内存到内存池即可
	void Clear();                      //清空内存池,重新初始化
private:

	b2Chunk* m_chunks;     //chunk数组,即多个大块内存的集合
	int32 m_chunkCount;    //已使用的chunk数,用于判断m_chunks数组是否使用完毕,
                           //还可以用来获取已使用的最后一个chunk
                           //来求得新建的chunk应该在的位置
	int32 m_chunkSpace;    //可以存放的chunk数,可理解为m_chunks数组长度

	b2Block* m_freeLists[b2_blockSizes];//i类型(block类型)的chunk中空闲节点(block)

	static int32 s_blockSizes[b2_blockSizes];//block的b2_blockSizes种类型
                                              //一次赋值,终身受用
	static uint8 s_blockSizeLookup[b2_maxBlockSize + 1];//存放i(0~b2_maxBlockSize)大小在s_blockSizes数组中类型位置索引
	static bool s_blockSizeLookupInitialized;//判断lookup数组是否初始化过
};

头文件在注释中进行说明,不再详讲。

关于m_freeLists和chunk的关系不大理解的可以参考下图:


若某类型chunk使用完毕则在m_chunks数组中建一个该类型数组,同时改变相应m_freeLists的指向。

m_chunks数组使用完毕,则建新数组,新数组比旧数组大b2_chunkArrayIncrement,并将旧数组copy到新数组,销毁旧数组。

好像讲过头了,在下面遇到了再回来看看吧:-D

下面看看实现文件:

1.静态变量及chunkblock结构

int32 b2BlockAllocator::s_blockSizes[b2_blockSizes] = 
{
	16,		// 0
	32,		// 1
	64,		// 2
	96,		// 3
	128,	// 4
	160,	// 5
	192,	// 6
	224,	// 7
	256,	// 8
	320,	// 9
	384,	// 10
	448,	// 11
	512,	// 12
	640,	// 13
};
uint8 b2BlockAllocator::s_blockSizeLookup[b2_maxBlockSize + 1];
bool b2BlockAllocator::s_blockSizeLookupInitialized;

struct b2Chunk    //链表(大块内存)
{
	int32 blockSize;
	b2Block* blocks;
};

struct b2Block    //链表节点(区块)
{
	b2Block* next;
};

其中s_blockSizeLookupInitialized由编译器默认初始化为false,静态成员变量blockSizeLookup将在构造函数中根据s_blockSizeLookupInitialized 来判断是否初始化。

2.函数

构造函数

b2BlockAllocator::b2BlockAllocator()
{
	b2Assert(b2_blockSizes < UCHAR_MAX);

	m_chunkSpace = b2_chunkArrayIncrement;
	m_chunkCount = 0;
	m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));
	
	memset(m_chunks, 0, m_chunkSpace * sizeof(b2Chunk));
	memset(m_freeLists, 0, sizeof(m_freeLists));

	if (s_blockSizeLookupInitialized == false)
	{
		int32 j = 0;
		for (int32 i = 1; i <= b2_maxBlockSize; ++i)
		{
			b2Assert(j < b2_blockSizes);
			if (i <= s_blockSizes[j])
			{
				s_blockSizeLookup[i] = (uint8)j;
			}
			else
			{
				++j;
				s_blockSizeLookup[i] = (uint8)j;
			}
		}

		s_blockSizeLookupInitialized = true;
	}
}


初始化chunk数组的长度m_chunkSpace为每次的chunk数组增量b2_chunkArrayIncrement,设定已使用的chunk数量m_chunkCount为0。给数组分配内存,大小为m_chunkSpace * sizeof(b2Chunk)对空闲列表m_freeLists和chunk数组m_chunks0.

s_blockSizeLookup数组的初始化:

s_blockSizeLookup的下标i即为对象大小,它存放的是该对象大小需要的block种类在数组s_blockSizes的下标index(你可以理解为第index种类型

block的种类在s_blockSizes数组中,大小为0~s_blockSizes[0]的对象只需要申请s_blockSizes[0]大小的内存即够用(即第0种类型block),大小为s_blockSizes[0]~s_blockSizes[1]的对象只需要申请s_blockSizes[1]大小的内存即够用,依此类推。

那么我们要s_blockSizeLookup数组做什么?用来取得对象需要的block大小的种类下标index(你可以理解为第index种类型),利用这个在m_freeLists数组中寻找对应种类的空闲block。

 

析构函数

b2BlockAllocator::~b2BlockAllocator()
{
	for (int32 i = 0; i < m_chunkCount; ++i)
	{
		b2Free(m_chunks[i].blocks);
	}

	b2Free(m_chunks);
}

释放m_chunk数组中的chunk链表和m_chunk数组。


内存分配

void* b2BlockAllocator::Allocate(int32 size)
{
	if (size == 0)
		return NULL;

	b2Assert(0 < size);

    //申请的空间大于规定的最大值
    //直接申请,不放到链表中
	if (size > b2_maxBlockSize)
	{
		return b2Alloc(size);
	}
    //根据要申请的内存获取block类型索引值
	int32 index = s_blockSizeLookup[size];
	b2Assert(0 <= index && index < b2_blockSizes);

	if (m_freeLists[index])
	{
        //有同类型的未被使用的block
		b2Block* block = m_freeLists[index];
		m_freeLists[index] = block->next;
		return block;
	}
	else
	{//没有同类型的未被使用的block
		if (m_chunkCount == m_chunkSpace)//判断m_chunks是否使用完毕
		{
             //使用完毕则新建更大的数组
			b2Chunk* oldChunks = m_chunks;
			m_chunkSpace += b2_chunkArrayIncrement;
			m_chunks = (b2Chunk*)b2Alloc(m_chunkSpace * sizeof(b2Chunk));
             //将旧数组copy到新数组上
			memcpy(m_chunks, oldChunks, m_chunkCount * sizeof(b2Chunk));
			memset(m_chunks + m_chunkCount, 0, b2_chunkArrayIncrement * sizeof(b2Chunk));
             //销毁旧数组
			b2Free(oldChunks);
		}
        //获取m_chunks数组中第一个未使用的chunk位置
		b2Chunk* chunk = m_chunks + m_chunkCount;
        //建立链表,即给blocks指针赋chunk空间,里面放着n个block
		chunk->blocks = (b2Block*)b2Alloc(b2_chunkSize);
#if defined(_DEBUG)
		memset(chunk->blocks, 0xcd, b2_chunkSize);
#endif
		int32 blockSize = s_blockSizes[index];
		chunk->blockSize = blockSize;
		int32 blockCount = b2_chunkSize / blockSize;
		b2Assert(blockCount * blockSize <= b2_chunkSize);
        //根据block大小划分chunk,并连接链表
		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));
        //注意要置null,否则m_freeLists不知道该chunk已经用完了
		last->next = NULL;
        //指定下一个是空闲的
		m_freeLists[index] = chunk->blocks->next;
        //增加已使用的chunk数
		++m_chunkCount;
        //由于是新建的链表,从未被使用过,故返回第一个节点
		return chunk->blocks;
	}
}

_DEBUG中的代码是供调试使用的,在此不做分析。

参数size是申请的内存大小,寻找满足大小的block种类,在m_freeLists数组中寻找该种类空闲block,并返回。若不存在,不要急着在数组中新建存放对应种类block的chunk链表,首先要判断是m_chunks数组是否使用完毕,否则无法新建chunk链表。m_chunks使用完毕,则新建一个m_chunks数组并扩大b2_chunkArrayIncrementcopy旧数组的内容到新数组上,销毁旧数组。之后再新建存放对应种类block的chunk链表,返回链表头指针并将m_freeLists指向其next.

 

释放函数

void b2BlockAllocator::Free(void* p, int32 size)
{
	if (size == 0)
	{
		return;
	}

	b2Assert(0 < size);
    //申请的空间大于规定最大值
	if (size > b2_maxBlockSize)
	{
         //不是在内存池中申请的,故直接释放空间
		b2Free(p);
		return;
	}
     //获取block类型索引值
	int32 index = s_blockSizeLookup[size];
	b2Assert(0 <= index && index < b2_blockSizes);

#ifdef _DEBUG
	// Verify the memory address and size is valid.
	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);

	memset(p, 0xfd, blockSize);
#endif
	b2Block* block = (b2Block*)p;
    //next应该为原本的空闲空间
	block->next = m_freeLists[index];
    //返回空间到内存池,即设其为free,下次直接使用该空间
	m_freeLists[index] = block;
}

_DEBUG中的代码是供调试使用的,在此不做分析。

注意:在内存池中申请的必须返回空间到内存池,不能销毁空间。使用malloc,则必须使用free

另外,在代码最后竟然没有设置p为NULL,这样可以么?那么对p使用free后再次使用p,若p在堆上释放,那么p岂不成了野指针,若是返回内存池,但p仍然指向该地址,p岂不是仍然可以使用该块内存么?这可能造成危险吧?

 

清空函数

void b2BlockAllocator::Clear()
{
	for (int32 i = 0; i < m_chunkCount; ++i)
	{
        //释放chunk链表中的blocks,即清空m_chunks中chunk的内容
        //并未释放m_chunks中的chunk
		b2Free(m_chunks[i].blocks);
	}

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

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

只是清空m_chunksm_freeLists内容,并未释放空间,释放是在析构函数中完成。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值