分类: C/C++
2008-05-31 14:17:47
首先说说动态内存分配。在c语言里用的最多的是malloc和free,在c++则是new new[] delete 和delete[]. 这几个函数是动态内存分配的基础,最常用但也是最占用CPU资源的系统调用之一.而且在大量使用以后很容易造成内存的碎片。如果系统内存中的碎片太多,就会在分配大块内存的时候失败或者只能在虚拟内存上分配内存,这就是为什么有些程序在运行了2,3个小时以后很容易速度不稳定和容易崩溃的原因。另外一个重要的因素就是程序员在写程序的时候,经常会分配了内存而忘记释放。特别是写超过 10W行代码的时候往往忘记了在哪里分配了内存. 所以内存的管理对于游戏的稳定性是非常重要的问题,毕竟大家都是动不动玩上10个小时不休息的主。
目前比较流行的解决方法就是在系统提供的内存分配函数上面,写自己的内存管理函数。在C语言里重写malloc和free,对每个内存的分配和使用情况做跟踪记录。在C++里则是重载操作符 new和delete. 通过提供自己的库,可以很容易检测到memory leakage. 通过在程序开始的时候从操作系统分配到一块足够大的内存,在此基础上进行内存管理,还可以有效的防止内存泄漏,并且还可以支持对象复用技术,提高游戏的速度和稳定性。当然,你也可以使用一些memory leakage的检测工具来检查内存使用情况(比如 firefox memory leakage detection tool 或者 Visual leak detector)。
实际上,在游戏程序设计中,很少使用动态的内存分配,大部分的内存都是事先分配好的。即使是链表或者是树这一类的数据结构,也是用数组进行有效的模拟。
================分割线==================
下面说点代码里边应该注意的问题。在相关内存相关的注意事项中,排在第一位的是内存对齐问题。也就是说,一块内存的首地址,必须要能被2,4,8,16,32 或者64整除。 不同的CPU对于这个数字有不同的需要。
针对Intel最新发布的 Pentium Dual Core系列 Xenon系列,以及早些日子的 Pentium 4系列。推荐使用64 Bytes或者 128 Bytes的内存对齐。 因为在Pentium4 系列用,每当程序要进行内存访问的时候,CPU的一个预处理模块(Prefetch)会事先把内存中的数据读到Level1 cache中,并且每次读入的数据量是 64个 bytes(Pentium Xenon系列是128 bytes)。 如果没有进行内存对齐, 比如一个int占用4字节,第一个字节在前64bytes中,后3个字节在后64bytes中,那么CPU在读取这个int的时候就需要多从内存中拿一次数据, 会大大增加代码的运行时间。让我们看下例子:
__declspec(align(64)) int test[128]; // 64字节对齐
int * pInt = (int *)((char *)test + 1); // 没有对齐的指针
int * pInt2 = test; // 对齐的指针[Page]
int f1(void)
{
int i, k=0;
for(i = 0; i < 16; i++) k+=pInt[i];
return k;
}
int f2(void)
{
int i, k=0;
for(i = 0; i < 16; i++) k+=pInt2[i];
return k;
}
对照附件中的 VTune的测试结果(见附件1),我们可以看出非64bytes对齐的运行时间(clockticks值),几乎是对齐内存的运行时间的3倍。所以在使用动态或者静态内存的时候,最好注意内存的字对齐问题。在Visual Studio 中,可以用 __declspec(align(64))对静态变量,数组或者结构进行内存对齐。动态内存分配可以使用_aligned_malloc() 和 _aligned_free().
这些内存对齐的问题,当前的编译器一般都会帮你优化,但是如果要写自己的内存管理函数,就需要分外注意了。
================分割线==================
下面说一下结构数组问题。经常我们会用到结构数组,形式如下:
struct MyStructure{
int FirstNumber;
int SecondNumber;
int ThiredNumber;
int FourthNumber;
} StructureArray[100];
这种类型的数据结构,还有另外一种组织的方式,那就是数组结构,形式如下:
struct MyStructure{
int FirstNumber[100];
int SecondNumber[100];
int ThridNumber[100];
int FourthNumber[100];
} ArrayStructure;
至于这两种形式用哪种好,要根据具体情况来判断。一般来说,如果要对所有结构中的同一个成员进行连续的访问,比如要求100个结构中所有FirstNumber的和,使用第2种形式会快很多。如果要分别求出每个结构所有成员的和,第一种形式要快很多,中国自学编程网, 。
===========求所有结构第一个成员的和==========
// 错误的选择
for(i = 0; i < 100; i++) Sum += StructureArray[i].FirstNumber;
// 正确的选择
for(i = 0; i < 100; i++) Sum += ArrayStructure.FirstNumber[i];
============求每个结构所有成员的和===========
// 错误的选择
for(i = 0; i < 100; i++)
Sum = ArrayStructure.FirstNumber[i]
+ ArrayStructure.SecondNumber[i]
+ ArrayStructure.ThirdNumber[i]
+ ArrayStructure.FourthNumber[i];[Page]
// 正确的选择
for(i = 0; i < 100; i++)
Sum = StructureArray[i].FirstNumber
+ StructureArray[i].SecondNumber
+ StructureArray[i].ThirdNumber
+ StructureArray[i].FourthNumber;
我想道理不用多说大家也明白了吧, 具体到程序设计中要根据哪种操作用的多来决定数据的组织方式。
关于内存访问,还有很多很多需要注意的事项,比如aliasing问题,store forward问题等等,建议大家去参考intel关于pentium的文档.