5.2.1.1 基本的Stack内存分配器
现在很多游戏中流行使用Stack内存分配。无论何时一个新的游戏关卡载入,内存都为她分配好了。当这个关卡载入完成的时候,要么很少甚至没有动态内存分配发生。当这个关卡结束的时候,她会卸载了全部她使用的内存。使用它具有很大的意义,使用这些类型的内存分配一个类似栈的数据结构。
一个Stack内存分配器是很容易实现的。我们简单地通过malloc(),new或者全局的数组(在这种情况下内存将会在BSS段分配in which case the memory is effectively allocated out of the executable’s BSS segment)的分配一大片连续的内存块。通过一个指向栈顶的指针来维护她。所有的内存低于这个指针的地址被认为是在使用中,而在它上面的所有地址被认为是未分配的自由的空间。这个位于栈顶部的指针被初始化到最低地址,栈顶指针一开始应指向栈的底部。每个内存分配请求只需要简单的移动这个指针到需要的大小的位置即可。最接近的被申请的内存块要释放只要简单的把这个指针移到对应大小个位置即可。
这里有一个很重要的地方对于Stack分配器来说,内存是不能被任意的释放的。所有的内存释放必须做她们申请时的相反操作。我们可以强制的限制这些内存不允许被私自的释放。反而,我们可以规定一个函式用来将栈顶指针反方向滚动到之前标记的地址,这样处于当前栈顶和滚动的指针之间的内存块就会被释放。
有很重要的一点是,要始终把指针回滚到两个内存块的分界上,因为在另外的一些方面,新的内存分配需求需要跟随在这个指针之后进行分配。为了确保这能成功,一个Stack分配器经常会使用一个函式用来返回一个marker来标记当前的栈顶。回滚函式会将她的marker一起操作。这看起来就像图5.1.一个Stack分配器的接口一般看起来像这样:
class StackAllocator
{
public:
//Stack marker:表示当前栈顶,你只能回滚这个marker,不能任意的使用
typedef U32 Marker;
// 构造函式要有一个总分配内存大小
explicit StackAllocator(U32 stackSize_bytes);
// 这个会分配出一个指定大小的内存
void* alloc(U32 size_bytes);
// 返回当前栈顶的Marker
Marker getMarker();
// 回滚栈到之前的marker
void freeToMarker(Marker marker);
//清除全部的Stack(将marker回滚到0)
void clear();
private:
// ...
};
//双面Stack分配器
一个简单的内存块实际上需要包含两个Stack分配器的,一个从内存块的下面开始分配另外一个从内存块的顶端开始分配。一个双面的Stack分配器更加实用因为可以允许使用栈顶和栈底的把内存。在一些情况下,两个堆栈使用大小大致相同的内存,他们相遇在内存块的中间。在另外一些情况下,两个Stack之中的一个所占据的内存要远比另外一个大,但是所有的内存分配请求都可以安全的完成只要需要分配的总大小不超过两个Stack可以分配的总内存块大小即可。就像图5.2.
在Midway的Hydro Thunder街机游戏中,所有的内存分配都来自一个简单巨大的内存块,她依靠一个双向的栈分配器维护。栈底的内存用于装载和卸载关卡(赛道),栈顶的内存用在临时变量上,他们在每一帧都会被申请然后被释放。这种内存工作方式非常的棒,让Hydro Thunder这款游戏不会受到内存碎片造成的坏的影响(看5.2.1.4章节)。Steve Ranck 是Hydro Thunder的首席程式员,他在以后的章节中深度讲解了其内存分配方法。

近期迴響