人气 275

[游戏程序] C语言基本功教程系列(4) - 高效无错的内存访问 [复制链接]

九艺网 2017-3-10 17:02:18

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
大家周末好,希望一个星期的学习和工作没能把大家累垮,这样又可以在这里听Aear在这里讲废话了。这个周末的主题就是内存访问,主要是谈谈写程序时候关于使用内存的技巧,以及一些应该注意的地方。
================分割线==================
首先说说动态内存分配。在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;                               // 对齐的指针
int f1(void)
{
int i, k=0;
for(i = 0; i < 16; i++) k+=pInt;
return k;
}

int f2(void)
{
int i, k=0;
for(i = 0; i < 16; i++) k+=pInt2;
return k;
}

对照附件中的 VTune的测试结果(见附件1),我们可以看出非64bytes对齐的运行时间(clockticks值),几乎是对齐内存的运行时间的3倍。所以在使用动态或者静态内存的时候,最好注意内存的字对齐问题。在Visual Studio .Net中,可以用 __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.FirstNumber;
// 正确的选择
for(i = 0; i < 100; i++) Sum += ArrayStructure.FirstNumber;

============求每个结构所有成员的和===========
// 错误的选择
for(i = 0; i < 100; i++)
    Sum =    ArrayStructure.FirstNumber
              + ArrayStructure.SecondNumber
              + ArrayStructure.ThirdNumber
              + ArrayStructure.FourthNumber;
  
// 正确的选择
for(i = 0; i < 100; i++)
    Sum =    StructureArray.FirstNumber
              + StructureArray.SecondNumber
              + StructureArray.ThirdNumber
              + StructureArray.FourthNumber;

我想道理不用多说大家也明白了吧, 具体到程序设计中要根据哪种操作用的多来决定数据的组织方式。
关于内存访问,还有很多很多需要注意的事项,比如aliasing问题,store forward问题等等,建议大家去参考intel关于pentium的文档.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-4-27 02:01 , Processed in 0.054400 second(s), 23 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.