上一篇文章中的b2BlockAllocator,作为从堆上分配小块内存,它让众多的小对象的创建销毁更加便捷。但同样还不够完善,作为从堆上分配的内存,使用时需要临时分配。使用完需要销毁。而如果该块内存只是在一个时间步有用到,岂不是很不划算。
栈内存能够比较好的解决这个问题——stack。看一下栈和堆的区别:
1、管理方式不同:栈由编译器管理;堆由程序员管理。
2、空间大小不同:win32中,堆可达4G;VC中栈默认1M(可以修改)。
3、碎片问题:堆易产生;栈不会。
4、生长方向不同:堆生长方向是向上的,也就是向着内存增加的方向;栈相反。
5、分配方式不同:堆是动态的,没有静态的堆;栈有两种:动态和静态。
6、分配效率不同:栈,系统提供底层支持,有专门的寄存器存放栈地址,效率高;堆,由库函数提供支持,效率底。
由于栈是在编译期间分配好的,不会动态的去改变。就像我们C语音中使用的数组一样。对于那种生命期很短的对象,直接在栈上保存,显然更加快捷。于是Box2D中便有了b2StackAllocator。
还是先看头文件,b2StackAllocator.h。public的方法除了构造及析构函数。还有:
void* Allocate(int32 size);
void Free(void* p);
int32 GetMaxAllocation() const;
分别是用来分配内存,释放内存,获得历史上最多被分配过多大内存。
private的属性则比较多,最重要的就是栈内存的空间啦.
使用 char m_data[b2_stackSize];来作为保存栈内存的容器。(果然是定义了一个大数组,然后在这段数组里做文章;)b2_stackSize就是编译时将会分配的栈内存大小。在这里为b2_stackSize = 100 * 1024; // 100k。
有意思的是这段内存保存在栈上,这个栈的意思是栈内存,是由系统自动维护的内存区域。不会由用户去操作。而在数据结构的概念上,同样有个栈结构,上面都说的栈内存是栈结构的系统实现,不过在b2StackAllocator中,对内存的Allocate与Free,也是按照栈结构去进行的。也就是后进先出。
属性 int32 m_index;代表当前m_data数据被分配掉的内存数目,
int32 m_allocation;代表的也是被分配掉的内存数目,和m_index有所区别的是,当m_data栈上内存不够用的时候,b2StackAllocator会从堆内存上分配一部分内存来使用,这个m_allocation也就会不管你是哪里的内存,都会被统计,m_index则只会统计m_data这段栈上的内存。
int32 m_maxAllocation;历史上最多被分配过多大内存,也就是GetMaxAllocation函数返回的值。在源代码中实际并没有用到这个函数和属性。
b2StackEntry m_entries[b2_maxStackEntries]; b2_maxStackEntries是个全局量,为32,意味着这个栈内存分配器最多分配32组内存。m_entries保存着每次分配的内存的入口,也就是内存头部的地址。而b2StackEntry是一个结构体。
struct b2StackEntry
{
char* data;
int32 size;
bool usedMalloc;
};
分别是每次分配的内存首地址,这块内存的大小,和这块内存是否用到了从堆上来分配。
int32 m_entryCount;代表当前分配了多少组内存。
这里欠个示意图,来说明整个b2StackAllocator中的各个属性什么意思。
b2StackAllocator.cpp
当然就是上面说的各个函数的实现啦。
构造函数就是把几个计数的参数初始化为零。
m_index = 0;
m_allocation = 0;
m_maxAllocation = 0;
m_entryCount = 0;
析构函数由于内存是在栈上,所以也没必要去释放内存。(部分在堆上分配的内存以及被free释放掉了),所以析构函数就声明了一下之前分配的内存,的确已经都被free了。(所以Allocate与free要记得成对使用)
b2Assert(m_index == 0);
b2Assert(m_entryCount == 0);
Allocate(int32 size)函数是本节的重点啦。思路呢,就是从那100K栈上的内存中,拿出size这么大内存,然后被b2StackEntry m_entries[b2_maxStackEntries];记录在案,每分配一次,m_entryCount计数增加一次。如果栈上内存不够了,就从堆上malloc一块内存,并且把这块内存标记为“被分配的”——usedMalloc=true。相对来说,代码就不那么重要了,照着这个思路去对照代码相信都可以看明白。不过还是把代码贴在这吧。
void* b2StackAllocator::Allocate(int32 size) { b2Assert(m_entryCount < b2_maxStackEntries); b2StackEntry* entry = m_entries + m_entryCount; entry->size = size; if (m_index + size > b2_stackSize) { entry->data = (char*)b2Alloc(size); entry->usedMalloc = true; } else { entry->data = m_data + m_index; entry->usedMalloc = false; m_index += size; } m_allocation += size; m_maxAllocation = b2Max(m_maxAllocation, m_allocation); ++m_entryCount; return entry->data; }
Free(void* p)有点局限性,它不会去p这个位置找分配了多少内存,然后释放掉它,而是去栈中先去找最近分配的这块内存,如果这块内存就是p这块内存的话,就释放它。说了这么多,其实就是栈的使用方法了。局限就在于,你先后Allocate了3块内存。哪么释放的时候,就一定要按照相反顺序来释放这3块。如果顺序错了,就没法释放掉。这个地方会有个疑问,那就是既然都按照顺序,又何必还要这个指针p呢,直接free()不就好了,就像pop一样,根本不用什么实参了。其实是可以的,至少从源码上,是可以这么修改的。但是这里要这么定义,我擦还是为了使用的方便吧,free(xx)至少知道你是释放的哪个对象。当你心里要释放的对象和实际释放的对象不符时,能够报错。而不是free()稀里糊涂就把最近分配的内存释放掉。
具体释放时候的策略,则是判断如果是从堆上分配的内存,则把堆内存释放掉。如果不是的话,更方便了。直接讲栈内存计数m_index减去这段内存的大小。这样下次需要Allocate()的时候,又会从m_index开始分配,之前释放的这块内存实际上还是会保存以前的东西,这有在下次分配并使用的时候,才会被新的数据覆盖。
void b2StackAllocator::Free(void* p) { b2Assert(m_entryCount > 0); b2StackEntry* entry = m_entries + m_entryCount - 1; b2Assert(p == entry->data); if (entry->usedMalloc) { b2Free(p); } else { m_index -= entry->size; } m_allocation -= entry->size; --m_entryCount; p = NULL; }
还有个GetMaxAllocation则是直接返回m_maxAllocation值,暂时不知道用处。当然,源码中也没用它。
int32 b2StackAllocator::GetMaxAllocation() const
{
return m_maxAllocation;
}
应用实例:
b2ContactSolver类的构造函数中先后分配了2块栈内存。(还没看到这块,所以不知道这个类啥用)
m_positionConstraints = (b2ContactPositionConstraint*)m_allocator->Allocate(m_count * sizeof(b2ContactPositionConstraint));
m_velocityConstraints = (b2ContactVelocityConstraint*)m_allocator->Allocate(m_count * sizeof(b2ContactVelocityConstraint));
然后会在内存m_positionConstraints中存储b2ContactPositionConstraint的对象数据。
b2ContactPositionConstraint* pc = m_positionConstraints + i;
pc->indexA = bodyA->m_islandIndex;
pc->indexB = bodyB->m_islandIndex;
pc->invMassA = bodyA->m_invMass;
pc->invMassB = bodyB->m_invMass;
pc->localCenterA = bodyA->m_sweep.localCenter;
pc->localCenterB = bodyB->m_sweep.localCenter;
pc->invIA = bodyA->m_invI;
使用完后释放
m_allocator->Free(m_velocityConstraints);
m_allocator->Free(m_positionConstraints);
注意释放顺序和分配顺序相反。