内存对程序效率的影响很大。即使是好的算法,用了不正确的内存分配,仍然会有效率问题。
内存对效能的影响有两方面:
- 动态内存分配(dynamic memory allocation)非常慢。它慢主要有两个原因。首先,堆分配器必须处理任何大小的分配请求;其次,在多数操作系统上,malloc()/free()必然会从用户模式(user mode)切换至(kernel mode),处理请求,再切换回来,这种上下文切换(context-switch)会非常耗时。
软件访问效能受其内存访问模式(memory access patter)主宰。数据置于连续内存块,比起置于广阔内存地址要更高效(cache等原因)。
因此,游戏开发中一个常见的经验法则是:维持最低限度的堆分配,并且永不在紧凑循环中使用堆分配。
我们可以不使用操作系统提供的分配器,而根据具体情况,使用自定义的分配器。多数游戏引擎会实现一个或多个定制分配器(custom allocator)。他比操作系统分配器更优的原因有二:
定制分配器从预分配的内存中完成分配请求。这样就不需要进行上下文切换。
- 通过对定制分配器的使用模式做出多个假设,定制分配器便可以比通用的堆分配器高效。
几种常见的定制分配器:
池分配器(pool allocator)
主要用于分配内存大小固定的情况。
池分配器的工作方式如下。首先,池分配器会预分配一大块内存,其大小刚好是分配元素的倍数。池内每个元素会加到一个存放自由元素的链表。池分配器收到分配请求时,就会把自由链表的下一个元素取出,并传回该元素。释放元素之时,只需简单地把元素插回自由链表中。
注意!储存自由元素的链表可以实现为单链。但是这样就要花费额外空间来存储链表的指针。意识到,自由列表内的内存块,按定义来说就是可用的内存。那为什么不用这些内存本身来储存自由列表的“next”指针呢?只要元素尺寸≧sizeof(void*),就可以使用这个小诀窍了。
若元素尺寸小余指针,则可以使用池元素的索引代替指针去实现链表。例如元素大小为16位,那么池里元素个数不超过2^16即可。(因为自由列表的内存是连续的,用索引可以算出其地址)。
具体实现
先看头文件的申明。
// freelist.h
class Freelist
{
public:
Freelist* m_next;
};
class FreelistHead: private Freelist {
public:
FreelistHead(int num, size_t elementSize);
~FreelistHead();
inline void* Obtain(void);
inline void Return(void* ptr);
private:
void * m_start;
};
Freelist
类是链表的每个结点。FreelistHead
类是链表的头结点,也代表了内存分配器。
测试代码如下:
//main.cpp
int main() {
FreelistHead freelist(2, 64); // elementsize >= sizeof(void*)
void* object0 = freelist.Obtain();
cout<<object0<<endl;
void* object1 = freelist.Obtain();
cout<<object1<<endl;
// obtained slots can be returned in any order
freelist.Return(object1);
freelist.Return(object0);
return 0;
}
类成员函数的具体实现如下:
//freelist.cpp
FreelistHead::FreelistHead(int num, size_t elementSize) {
union element {
void* as_void;
char* as_char;
Freelist* as_self;
}start, end, now, nxt;
unsigned long step = elementSize/8;
start.as_void = malloc(num*elementSize);
end.as_char = start.as_char + (num*step);
m_start = start.as_void;
// initialize the free list - make every m_next of each element point to the next element in the list
m_next = start.as_self;
for (now=start, nxt.as_char = start.as_char + step;
nxt.as_char < end.as_char;
now=nxt, nxt.as_char = nxt.as_char + step) {
now.as_self->m_next = nxt.as_self;
}
now.as_self->m_next