在某些场合下,我们会使用到非常小的对象(甚至可能只有数byte),其生命周期可能也很短,如果每次都通过malloc或者new在堆上分配内存,用后销毁,效率太低。于是维护一些不定尺寸并可扩展的内存池,当小对象需要内存分配时,直接从早已准备好的内存池上返回一块大小合适的内存。注意,当内存使用完毕后,我们需要返回其内存到内存池中,而不是销毁它。
本文以Box2D为例对SOA(smaller object allocator)进行分析。
首先对Box2D中chunk(大块内存)和block(区块)进行说明,如下图:
一个由多个16byte大小block组成的chunk,实际上就是一个链表。chunk是一个由n个block组成的固定大小的大块内存。而多个大块内存形成内存池。
首先看一下头文件
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.静态变量及chunk和block结构
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_chunks设0.
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_chunkArrayIncrement,copy旧数组的内容到新数组上,销毁旧数组。之后再新建存放对应种类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_chunks和m_freeLists内容,并未释放空间,释放是在析构函数中完成。