我的个人理解一个好的内存分配器,1要分配内存块,2要释放内存块,3减少碎片。这3个是起码的,当然你可以加GC功能,在内存管理这本书里面讲了2个GC方法(作者应该很牛逼,他说他所有讲的内存管理都是最简单,为说明问题,代码没有一点优化,否则就会晦涩难懂,还好U3并没有写成这样)
" i# M# {/ @$ V( h* D; H
我先说说我以前遇到几种内存管理方法,一个很大的问题,就是当内存申请不够用了的时候,再申请一块的时候,都没讲怎么处理。都是预先分配一个大的,如果不够用了怎么办?还有就是,内存碎片很难处理,因为你做内存整理,牵扯到指针的移动的改变,做这个真是费力不讨好,速度就要有限制,还有有空间来配合做移动。其余的就是速度和空间上的交换了。所以这些方法都很难找到平衡点,没有一个最有效折中的方法。
6 H7 u j' X2 |, J. h
当然U3也并没有做到你想像的那么好,但它做的比较平衡。性价比高。. B! y- e' C$ L! \# y
先说几个统计性的数据,在游戏中,小于1K的动态分配数据是最频繁的,而且越小越频繁。相反越大的数据,例如模型VB,IB,texture数据等等吧,这些申请和释放频率比较低,很大多数动态分配时间都浪费到小数据的申请和释放上。有2种比较内存管理机制,一个是典型的空间换时间,就是一个内存块,我分成相等的小块,根据某种机制正好要在这个里面分配,那么不管这个小块大于要申请的size多大,都要拿出一个小块。比如我一个内存块是40,我分成4块,每个大小是10.我现在要分配6,那么取出一个小块,多出那个4就要被浪费掉了。还有一种机制就是有多大我就分多大的,然后用链表或者树结构管理空间和占用的块。这种方法经过多次申请释放会有很多碎片是不可用的。第一种机制,分配释放都很块,因为这个是等大的可以做类似数组那么用索引来出来,申请释放。而第2种机制申请和释放去查找,比较浪费时间。
5 l4 |; Q; T4 o4 L. F( e, F
U3方法用的是第一种机制,最重要的是,它巧妙应用了操作系统的特性。如果大块内存也申请释放相对比较频繁的话,可以把大块内存这个用第二种方法处理。但这个频繁要多么频繁,好像很难给出准确数据,因为所有的时间消耗都是小块内存数据申请上,所以我感觉不用也可以。/ ~; x5 q. N, {2 W5 n4 L5 b
4 b; T9 o. }5 r1 A
说了这么多,现在言归正传,详细分析U3在windows下的内存管理,因为U3支持多个平台,每个平台下的内存管理是不一样,这个也是针对不同平台的特性定制的,后面你就会发现,这个Windows下的内存管理,巧妙的运用了windows内部原理。
(这里我改了好几遍,不知道怎么说好,这些说原理吧,首先是不容易说明白,看的人一定也一头雾水,最后还是把代码弄上来,分析代码,这样说的能比较透彻。有unreal代码的自己去找FMallocWindows.h这个文件)+ o. a9 O- g1 i1 \( i
, r2 Z7 i' {$ Q' F0 C. O
struct FPoolInfo$ U8 C6 c, [. V$ V. O; |' X$ V
{
DWORD Bytes; // Bytes allocated for pool.( P+ f" Y8 v/ q# A5 L! o
DWORD OsBytes; // Bytes aligned to page size.5 g9 O$ \) b" w2 L* R. Y# B% s
DWORD Taken; // Number of allocated elements in this pool, when counts down to zero can free the entire pool." q) G* p5 m8 O* \. l
BYTE* Mem; // Memory base.
FPoolTable* Table; // Index of pool.* q4 m* k/ T: Y0 H% R/ V
FFreeMem* FirstMem; // Pointer to first free memory in this pool.
FPoolInfo* Next;( @; O0 P9 ^$ a& W5 |% k5 z# e
FPoolInfo** PrevLink;
4 O( |& k6 ~$ s) ]& |6 C
void Link( FPoolInfo*& Before )0 F- [8 {; \, p# X" D
{: r5 ^' D# r3 b* c1 X! g
if( Before )$ g9 ]+ r. L# H* z6 g0 U9 J/ d
{
Before->revLink = &Next;8 g' X/ t( B% G: v! n' m$ B6 l
}
Next = Before;3 L9 v f" X. R6 j+ [; ~3 L* T2 I
PrevLink = &Before;: G! u8 L# k1 n4 }- d! d
Before = this;
}
void Unlink()
{. D! i- }8 R! h
if( Next )
{$ F7 K5 q6 P+ }- a9 v2 O
Next->revLink = PrevLink;
}3 I6 z8 Z* M0 |) r+ l2 \
*PrevLink = Next;
}
};
& {2 } n6 f' Z2 W o9 q$ ], b
// Information about a piece of free memory. 8 bytes." E+ b% T8 Y' Y1 N5 q' {: @7 g
struct FFreeMem9 w" G1 T7 c$ x! q: a& I+ U, V
{1 _6 j! h4 v4 o/ \
FFreeMem* Next; // Next or MemLastPool[], always in order by pool.- [5 o7 B" r# G# F6 E! i) v
DWORD Blocks; // Number of consecutive free blocks here, at least 1.
FPoolInfo* GetPool()0 o5 I( M' T) b+ O4 `" l
{! J& o, M1 E4 m- k2 k; G9 O, @8 b
return (FPoolInfo*)((INT)this & 0xffff0000);
}. h, \" V. G) O: z
};
: n# S8 b+ o: t8 j/ M& c
// Pool table.4 b; ~$ j0 _7 y
struct FPoolTable, ?+ w: z4 h' A. X) l' y& P( }, \
{: c7 b+ w. {( f+ b. L- V
FPoolInfo* FirstPool; //所有小块没都分配出去的FPoolInfo列表
FPoolInfo* ExhaustedPool;//所有小块都分配出去的FPoolInfo列表
DWORD BlockSize;//一次申请分配,能分配的内存大小(也就是说小块内存大小)
};
B% U/ Q' o6 ^$ s# a
当分配一个大块内存的后,这个大块内存地址会记录到FPoolInfo的 BYTE* Mem这个变量,每个FPoolInfo都要归属于一个FPoolTable,也就是说FPoolTable管理能分配小块内存都等于BlockSize的所有FPoolInfo。开始创建这个大块内存的时候,同时这个FPoolInfo也被创建,进入FPoolInfo* FirstPool列表,如果这个FPoolInfo小块内存(上面讲的第一种机制,大块会切分相等小块,小块就是去分配的,小块的大小是FPoolTable的BlockSize变量)都分配完毕,那么就进去ExhaustedPool链表。