C语言基本功教程系列(3) - 快速的函数调用 

2006-12-06 15:54 发布

2276 0 0


我又来了,今天坎坎函数调用的问题。函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。即使在游戏的关键循环中,调用几十个函数也是很常见的。所以函数调用代码的质量,在很大程度上影响着游戏的质量。

还是先说最基本的代码风格问题。首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型
=========不好的风格==========
void function(char * ServerName)
{
   // 内部不允许对ServerName的内容进行修改
}

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

为什么这么做呢? 举个简单的例子: 在团队开发中程序员A写好了displayFunction,传了一个数据结构给displayFunction做图象显示,然后在接下来的程序中对数据进行计算。A认为displayFunction不会对数据进行修改,所以在以后的数据运算中,没有进行一致性检测。过了几天程序员B被派过来优化A的程序,因为不知道不能改数据,结果改了下,在displayFunction中改变了数据结构的内容,当时测试通过。但是在产品发布的Alpha测试阶段,用real data的时候出了问题。我想通宵debug去差这么点个小问题,不是很值得吧。只要稍微留点心,就可以避免了

==================分割线==================
下面谈谈函数的调用问题。我们都知道,在调用的一个函数的时候,传给函数的参数是要压到栈里,然后才能被函数访问。我们来看一下函数调用的汇编代码.(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02)

=======printf("%s%d%d%d%d",haha,m,n,p,i);======
00401000 push ecx
00401001 push ebx
00401002 mov ebx, dword ptr [esp+04]
00401003 push ebp
00401004 mov ebp, dword ptr [esp+08]
00401005 push esi
00401006 push edi
00401007 mov edi, dword ptr [esp+10] 
00401008 xor esi, esi
00401009 push esi
0040100A push edi
0040100B push ebx
0040100C push ebp
0040100D push 00408040
0040100E push 004060FC
0040100F call 00401054

我的天哪,这是多少代码,只不过为了把参数push到栈里就用了15条。看我们看看另一段代码

===========printf("%s",haha);============
00401010 push 00408040
00401011 push 004060FC
00401012 call 00401054

现在我不用说大家都明白了吧。传递给函数的参数越少越好,最好就是一个指针,指向一个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);


==================分割线==================
这个例子不是很好,因为降低了代码的可读性,不过做为参考。。。。
很多人喜欢写代码的时候这么写:
char szName[] = "Aear";
int length;

length = strlen(szName);
if(length > 0)   // 这行的效率不考虑
{
   // do something
}
粗一看没什么问题,不过如果length在以后用不到的话,那么就浪费了。因为length占用了内存,而且浪费了cpu资源。让我们看带汇编代码(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02)

length = strlen(szName);
if(length > 0)   {...}

0040101F       sub eax, edx
00401021       mov dword ptr [esp+4], eax // 把返回值存到length中
00401025       je 00401039                       // 判断跳转

========更快速的写法的代码========
if(strlen(szName)) {...}

0040101F       sub eax, edx
00401021       mov esi, eax   //把返回值放在个临时寄存器中
00401023       je 00401037

大家都知道寄存器之间进行数据操作是非常快的,而且是稳定的一个cpu clock cycle,至于00401021       mov dword ptr [esp+4], eax 到底要花多少个clock cycle,那只有天知道了。因为这种从内存中读数据的指令,最少也是2个clock cycle,即使在L2 cache中,也不会比 mov esi, eax 快,而且浪费了栈空间。


==================再分割下吧,虽然不是很喜欢==================
最后说说一种类告诉的分枝判断参数传递。在有些情况下,我们经常要传很多参数,比如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 );

就说这么多,累了,下次见

楼主新帖

TA的作品 TA的主页
B Color Smilies

你可能喜欢

C语言基本功教程系列(3) - 快速的函数调用 
联系
我们
快速回复 返回顶部 返回列表