基于OpenGL编写一个简易的2D渲染框架-12 重构渲染器-BlockAllocator
Posted 为了邮箱5
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于OpenGL编写一个简易的2D渲染框架-12 重构渲染器-BlockAllocator相关的知识,希望对你有一定的参考价值。
BlockAllocator 的内存管理情况可以用下图表示
整体思路是,先分配一大块内存 Chunk,然后将 Chunk 分割成小块 Block。由于 Block 是链表的一个结点,所以可以通过链表的形式把未使用的 Block 连接起来,并保存到 pFreeLists 中。当我们向 BlockAllocator 申请一块内存时,BlockAllocator 会通过 pFreeLists 表索引出一块空闲的 Block,并返回其地址。当我们不断申请内存的时候,BlockAllocator 会不断返回未使用的 Block 块。当 Chunk 内存空间用尽时,BlockAllocator 会再开辟一块新的 Chunk。
但是,分割 Chunk 时,Block 的大小是确定的。假如 Block 的大小为 148 字节,当只申请 64 字节大小的空间时,会造成浪费;当申请 256 字节时,Block 的大小不足;
所以需要把 Block 根据大小分成不同的类型,如 64、148、256 ...,当申请内存的时候,BlockAllocator 先确定要申请内存的大小,再查找表,返回适当大小类型的 block。但是如何快速返回适当大小类型的 block?可以使用一个数组:
static int blockTypeSizeTable[BLOCK_TYPE_COUNT];
先通过下面的方式初始化 blockTypeSizeTable:
/* 设置索引数组 blockTypeSizeLookup 的 block 类型索引值 */ if ( blockTypeSizeLookipInitialized == false ) { blockTypeSizeLookipInitialized = true; int blockTypeSizeIndex = 0; for ( int i = 0; i <= MAX_BLOCK_SIZE; i++ ) { if ( i <= blockTypeSizeTable[blockTypeSizeIndex] ) { blockTypeSizeLookup[i] = blockTypeSizeIndex; } else { blockTypeSizeIndex++; blockTypeSizeLookup[i] = blockTypeSizeIndex; } } }
假设有三种类型大小的 Block,16 字节、32 字节、48字节,则数组 blockTypeSizeTable 的大小为 49, 初始化后的 blockTypeSizeTable 储存的内容是:
0-16 储存索引 0
17-32 储存索引 1
33-48 储存索引 2
当我们申请大小为 36 字节的内存时,由于 36 落在区间 33-48 内,所以 blockTypeSizeTable[36] 会得到索引 2。然后通过 2 查找表 pFreeLists 即可获取大小为 48 字节的 Block。pFreeLists 是一个数组,储存所有类型 Block 链表的地址。pFreeLists[0] 指向的是大小为 16 字节的空闲 Block 链表,pFreeLists[2] 指向的是大小为 32 字节的空闲 Block 链表。
具体的分配操作在函数 allocate 中:
void* BlockAllocator::allocate(int size) { if ( size == 0 ) return 0; assert(size > 0); /* 使用四个字节记录 block 的类型索引,free 是使用 */ size += sizeof(int); /* 申请的空间大于规定的最大值,直接申请,不放到块的链表中去 */ if ( size > MAX_BLOCK_SIZE ) { int* data = ( int* ) malloc(size); /* -1 表示这是直接分配的内存 */ data[0] = UNKNOWN_MEMORY; return (data + 1); } int index = blockTypeSizeLookup[size]; assert(0 <= index && index < BLOCK_TYPE_COUNT); /* 存在同类型的未被使用的内存块?返回内存块 */ if ( pFreeLists[index] ) { /* 使块头指针指向新的未被使用的 block */ Block* block = pFreeLists[index]; pFreeLists[index] = block->next; return (( int* ) block + 1); } else { /* 扩展 chunk 数组 */ if ( nChunkCount == nChunkSpace ) { Chunk* oldChunks = pChunks; nChunkSpace += CHUNK_ARRAY_INCREMENT; pChunks = ( Chunk* ) malloc(nChunkSpace * sizeof(Chunk)); memcpy(pChunks, oldChunks, nChunkCount * sizeof(Chunk)); memset(pChunks + nChunkCount, 0, CHUNK_ARRAY_INCREMENT * sizeof(Chunk)); ::free(oldChunks); } int chunkSize = chunkSizeTable[index]; /* 获取一个未被使用的可以用来分配内存的 chunk */ Chunk* chunk = pChunks + nChunkCount; chunk->blocks = ( Block* ) malloc(chunkSize); /* 获取当前申请的 block 类型大小 */ int blockSize = blockTypeSizeTable[index]; /* 计算一块 chunk 内存能够分割成 block 的数量 */ int blockCount = chunkSize / blockSize; assert(blockCount * blockSize <= chunkSize); /* 将 chunk 分割出许多 block,再将 block 以链表的形式串起来 */ for ( int i = 0; i < blockCount - 1; i++ ) { Block* block = ( Block* ) (( char* ) chunk->blocks + blockSize * i); Block* next = ( Block* ) (( char* ) chunk->blocks + blockSize * (i + 1)); block->sizeIndex = index; block->next = next; } /* 将最后一个 block 的 next 指向空结点,表示这是最后一个 block */ Block* lastBlock = ( Block* ) (( char* ) chunk->blocks + blockSize * (blockCount - 1)); lastBlock->sizeIndex = index; lastBlock->next = nullptr; /* 将刚申请的 block 链表的第二块 block 保存到 pFreeLists 对应类型的数组中 */ pFreeLists[index] = chunk->blocks->next; nChunkCount++; /* 返回刚申请的 block 链表的第一块 block */ return (( int* ) chunk->blocks + 1); } }
根据申请内存的大小获取 Block 类型的索引 index,然后通过 index 查找表 pFreeLists:
1、存在未使用 Block,返回 Block,并使 pFreeLists 指向下一个未使用的 Block。
2、不存在未使用 Block,申请一块 Chunk,分割 Chunk 为 Blocks,返回首 Block,并使 pFreeLists 指向下一个未使用的 Block。
值得注意的是:Block 内存的前四个字节是用来储存 Block 类型的信息 sizeIndex,所以在返回 Block 内存地址的时候,向后偏移了 4 个字节。通过申请内存大小索引 Block 类型时已经将 size 多添加了 sizeof(int) 个字节的大小。
当使用完 Block 后并返还给 BlockAllocator 时,需要知道当前 Block 的类型才能正确添加 Block 到对应类型的未使用链表中,所以前面要用四个字节的大小储存器类型信息 sizeIndex:
void BlockAllocator::free(void* ptr) { int* data = ( int* ) ptr - 1; int index = data[0]; if ( index == UNKNOWN_MEMORY ) { ::free(data); return; } /* 根据内存大小获取 block 类型的索引值,并判断是否有效 */ assert(0 <= index && index < BLOCK_TYPE_COUNT); int size = blockTypeSizeTable[index]; /* 用头插法将 block 插到 pFreeLists[index] 指向的 block 链表中去 */ Block* block = ( Block* ) data; block->next = pFreeLists[index]; pFreeLists[index] = block; }
通过返回的地址向前偏移四个字节,获取 Block 类型信息,然后插入到未使用 Block 链表中。
当申请的内存过大时,BlockAllocator 会直接使用 malloc 函数分配内存(没有合适大小的 Block),并标记为 UNKNOWN_MEMERY。所以在释放时会调用 free 函数释放。
通过 BlockAllocator 可以实现顶点数据的内存管理。
以上是关于基于OpenGL编写一个简易的2D渲染框架-12 重构渲染器-BlockAllocator的主要内容,如果未能解决你的问题,请参考以下文章
基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader