C语言基本功教程系列锦集

///
// 本锦集整理制作:honeymc
// 原文作者:aear 原文链接自http://data.gameres.com/document.asp?TopicID=65354
///

===================== 循环篇 =======================

两个循环的比较:

int i;
for(i = 0; i < 100; i++)
{
    // do something
}

更快的循环:

i = 100;
do
{
    // do something
}while(--j);

====================== if语句篇 之 风格 ======================

不好的风格和好的风格的比较:

-------- 不好的风格 ----------

if( (x +4-y * 25) > 10 || y > 1023 || GetSomething())
{
   ....
}

-------- 好的风格 ----------

if( (x +4-y * 25) > 10 
    || y > 1023 
    || GetSomething() )
{
   ....
}

====================== if语句篇 之 表达式1 ======================

一定要把最容易成立的条件放在最前面进行判断,这是涉及到CPU的branch prediction的问题.简单的说,CPU有个指令缓存,会预先把一部分代码读到缓存中等待稍后执行.当CPU遇到 if语句的时候,会把条件判断为true的那段代码读到缓存中,然后对if(条件判断)中的条件判断语句进行运算.如果运算结果是false,那么CPU就会重新从内存中载入false的代码,在这期间大部分CPU时间会被浪费掉.比如:

-------- 错误的写法 ----------

if( (float)rand() / RAND_MAX < 0.2 )  //只有20%的可能运行if部分
{
    // 被读入到指令缓存的部分.
}


-------- 正确的写法 ----------


if( (float)rand() / RAND_MAX > 0.2 ) //有80%的可能运行if部分
{
    // 被读入到指令缓存的部分。
}

====================== if语句篇 之 表达式2 ======================

根据C语言的规则(这点不同于Pascal),如果第一个条件(rand() / RAND_MAX < 0.4)不成立,那么就不会运行第2和第3个条件,而直接跳转。所以应该把最难成立的条件放在第一的位置上,正确的代码为:

if (  (float)rand() / RAND_MAX < 0.2     // 只有%20的可能
      && (float)rand() / RAND_MAX < 0.3
      && (float)rand() / RAND_MAX < 0.4 ) 
{
    ......
}

由于编译器并无法计算和统计每种条件成立的可能性,只能靠大家手动的调整来提高代码的效率。

====================== if语句篇 之 binary branch ======================

最后是if有一种技术叫做binary branch,举个简单的例子,代码如下:

int x;
if( x == 1)
{
}
else if( x == 2)
{
}
else if( x == 3)
{
}
else if( x == 4)
{
}

对付这段代码,可以用switch来解决,也可以用binary branch,修改后的代码如下:

if( x <= 2)
{
     if( x == 1)
     {...}
     else
     {...}
}
else
{
     if( x == 3)
     {...}
     else
     {...}
}

如果判断的情况复杂一点,编译器就没有优化的能力,需要考大家自己动手啦。

====================== if语句篇 之 aear总结 ======================

这坟挖的。说实话,这些技巧也就是在 386, 486 那些10多年前的机器上还有些价值。对于现在的机器来说,这点速度的提升已经没什么大用了。不过对于理解CPU的结构有些帮助.

顺便说下,如果使用switch语句,并且所有的判断条件是能够用0到一个整数表示, 比如:

switch( num )
{
case 0:
    ...
    break;
case 1:
    ...
    break;
case 2:
    ...
    break;
}

或者

enum TYPE {
     TYPE1, 
     TYPE2, 
     TYPE3, 
};

switch( Type )
{
   case TYPE1:
      ...
   case TYPE2:
      ...
   case TYPE3:
      ...
}

那么switch会被优化成使用函数指针表进行跳转, cost是 o(1)


====================== 快速的函数调用篇 之 函数的const参数 ======================

首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型

-------- 不好的风格 --------

void function(char * ServerName)
{
   // 内部不允许对ServerName的内容进行修改
}

-------- 好的风格 --------

void function(const char * ServerName)
{
   // 内部不允许对ServerName的内容进行修改
}

====================== 快速的函数调用篇 之 函数的调用问题 ======================

我们都知道,在调用的一个函数的时候,传给函数的参数是要压到栈里,然后才能被函数访问。所以传递给函数的参数越少越好,最好就是一个指针,指向一个structure。这就是为什么大部分的directX的函数就是一个指针的大structure传过去。里边的参数好几十个。当然了 void fucntion(void)是最快的函数调用,也可以用inline来优化关键循环内的函数。不过在每一个frame的执行代码中,有成百上千个函数,不可能所有的都inline吧。所有能快点就快点喽。当然了,传递structure的reference也是同样的效果,只要不把structure当参数就好

-------- 错误的方式 --------

void function(struct OneStructure Parameter);

-------- 正确的方式 --------

void function(struct OneStructure & Parameter);
or:
void function(struct OneStructure * pParameter);

====================== 快速的函数调用篇 之 位操作 ======================

最后说说一种类告诉的分枝判断参数传递。在有些情况下,我们经常要传很多参数,比如pixel shader等等,这些函数根据参数的设置,进行不同的操作。举个例子:

struct Parameter{
     bool bDrawWater;
     bool bDrawSkybox;
     bool bDrawTerrain;
     bool bDrawSepcialEffects;
} DrawParamter;
void DrawEnvironment( struct Parameter * pPara)
{
     if(pPara->bDrawWater) {....};
     if(pPara->bDrawSkybox) {....};
     if(pPara->bDrawTerrain) {....};
     if(pPara->bDrawSpecialEffects) {....};
}

对于这样的代码,还有更快速, 更节省内存的方法,那就是位操作。

const static UINT32 DRAW_WATER_FLAG               = 1;
const static UINT32 DRAW_SKYBOX_FLAG              = 1 << 1;
const static UINT32 DRAW_TERRAIN_FLAG             = 1 << 2;
const static UINT32 DRAW_SPECIALEFFECTS_FLAG  = 1 << 3;
void DrawEnvironment(UINT32 DrawFlag)
{
    //注意了,这里不需要 pPara->,也就是节省了内存访问,速度至少提高了1到2个clock cycle
     if( DrawFlag & DRAW_WATER_FLAG ) {.....};
     if( DrawFlag & DRAW_SKYBOX_FLAG) {.....};
    //甚至还可以进行各种不同组合的判断,比如
     if( DrawFlag & (DRAW_WATER_FLAG | DRAW_SKYBOX_FLAG) ) {....};
}

在调用的时候,代码更加简洁明了:

DrawEnvironment( DRAW_WATER_FLAG | DRAW_TERRAIN_FLAG );

====================== 高效无错的内存访问篇 之 动态内存分配 ======================

首先说说动态内存分配。在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)。

实际上,在游戏程序设计中,很少使用动态的内存分配,大部分的内存都是事先分配好的。即使是链表或者是树这一类的数据结构,也是用数组进行有效的模拟。

====================== 高效无错的内存访问篇 之 内存对齐 ======================

这些内存对齐的问题,当前的编译器一般都会帮你优化,但是如果要写自己的内存管理函数,就需要分外注意了。

====================== 高效无错的内存访问篇 之 结构数组 ======================

下面说一下结构数组问题。经常我们会用到结构数组,形式如下:

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种形式会快很多。如果要分别求出每个结构所有成员的和,第一种形式要快很多。 具体到程序设计中要根据哪种操作用的多来决定数据的组织方式。

====================== 末篇 ======================

今天的主题是文件访问,Socket,和其他一些值得注意的内容。文件和Socket比较类似,都是在说IO访问,不过在操作系统级别上的实现有很大不同。IO访问无论从什么角度讲,都是计算机系统里最慢的操作。特别是在游戏制作中,动不动就几百兆的动态或者静态数据,贴图纹理,和各种音响音乐等。要一次性把这些所有的数据读到内存中是不大可能的,所以在游戏进行过程中要不短的从硬盘或者光驱里读文件。 如何能够最小化这个瓶颈, 是值得注意的问题。先从文件说起吧。

====================== 末篇 之 文件 ======================

C语言里,正常的文件操作一般是3步。
fp = fopen(XXXX,XXX);
读写操作....
fclose(fp);

首先要注意的是,fopen里边要用binary mode打开文件,不要用ASCII mode. 很多人在处理文本类型的文件时候,喜欢用ASCII mode,然后用fgets一行一行的读。实际上ASCII mode无论如何操作,都是非常慢的,而且fgets函数更加的慢 [1]。所以即使是文本文件,也要用binary mode打开,一次读入一大块近来,慢慢处理。

其次需要注意的是,每一次读取文件的时候,硬盘都会对磁头进行重新定位和寻址。 这点会根据操作系统的不同而不同,总的来说windows XP要比 windows2000好点,但是也只是在系统文件方面 [2]。 因此每次读取的内容越多,平均效率就越高。同时操作系统提供磁盘缓存,当你写如磁盘的时候,只要不用fflush和fclose,数据在短时间内还是在内存中的,如果这时候再读出来写如的内容,也非常快。读写文件的时候不要用C++的流。写文件一次最好写不小于4k的数据, 而且最好对文件结构有效的安排,进行连续的访问(不要频繁使用fseek)[3]. 这些都有助于提高文件的访问速度。

最后,如上文中提到的,每次使用fclose和fflush的时候,都会强迫文件从缓存中写如到磁盘里。这个过程极其缓慢而且耗费时间,所以不在必要的时候,不要使用fclose和fflush. 如果一个文件读写完毕,而你又不确定是否短时间内会用到它,那就不要用fclose.你可以专门写一个类,管理这写文件的指针。对于经常会进行操作的文件,比如大地图的texture文件等,fopen一次就ok了,直到游戏结束再fclose

====================== 末篇 之 SOCKET ======================

SOCKET虽然也是IO访问,要比文件快多了。但是在recv的时候,还是一次读的越多越好。这样效率更高。下面说一些SOCKET编程的技巧。

1. 使用异步socket (asynchronous IO). 在网络程序设计中,又2种处理方式,第一种是对每一个连接请求,都使用一个线程或者进程,第2种是使用一个线程同时使用异步IO. 第一种方式虽然程序设计上简单,但是创立进程的时候一般会有一些时间用在建立context上,进程间的转换和mutex等也需要浪费很多CPU资源,总体来说不如异步 IO 有效率 [4]。 

2. 如果必须要使用多线程,可以考虑事先就创建好该线程,然后在需要的时候,把socket发过去就行了[4]。

3. 在处理 UDP协议的时候,需要注意的是,UDP和TCP不一样。UDP没有control flow,如果接收端的buffer满掉了,再来的UDP包都会被drop掉。所以在处理UDP协议的时候,一般需要专门一个线程读UDP包,防止过多的数据包丢失。

4. 最后,网络数据包多大合适? 这个很难说。对于UDP来说,小包是不划算的. 我们通常用的Ethernet(也就是LAN),在第2层Data link layer,最大的frame size 是1500 bytes,刨除最20 Bytes(最少)的IP头,8 bytes的UDP头,所有最大的UDP包可以包含 1472个bytes。要是考虑IP包有可能会有附加头信息,一般1400就比较合适。但是如果有些老版本router不地道,对你的UDP包分片的话,就比较惨了。能保证不分片的UDP包大小是513个byte左右 [5],不过毕竟现在这种老的Router很少了,1400字节大小的UDP包还是比较安全的。对于TCP来说,因为是stream protocol,不用考虑包的大小。但是TCP有个缺点,就是如果你一次发送很多很多数据,那么TCP的速度会一会快,一会慢(见[4]中的关于video streaming的介绍)。所以,需要程序调节,匀速发送数据。

====================== 末篇 之 其他 ======================

其他一些程序设计上的东西,很多可以参见 [6]

1. 能用UINT的地方就用UINT,因为很多是UINT最快,而且UINT的除法要比int快。
2. 尽量避免类型转换,如果最后要转成float,开始的时候就用float比较好。
3. 不要用double
4. 能用乘法就不要用除法。 比如   3/2 可以换成  3 * 0.5
5. struct的大小尽量是2的倍数,如果不是就调整下,加pad。因为可以在level1 cache里放整数个。
6. 全局变量少用,如果要用,加上static
7. 局部变量也是越少越少。这样register的效率更高
8. 能用switch的地方,就不要用if,因为switch是直接生成跳转表,速度快很多
9. 互相关联的代码之间不要空行,功能不同的代码之间最好空上1行区分开
10. 用const static 代替 #define 定义常量
11. 统一你的代码风格,始终使用同样的命名规则

====================== 最后 ======================

生成高效率代码永远是从使用更好更合适的算法开始,但是编译的时候不要忘记打开你的优化开关。希望这个系列给大家带来了一些有用的东西。下个系列见 88

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值