C语言基本功教程系列(3) - 快速的函数调用
我又来了,今天坎坎函数调用的问题。函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。即使在游戏的关键循环中,调用几十个函数也是很常见的。所以函数调用代码的质量,在很大程度上影响着游戏的质量。
还是先说最基本的代码风格问题。首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用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 );
就说这么多,累了,下次见
来自:http://data.gameres.com/document.asp?TopicID=65696 此信息由aear提交。