我又来了,今天坎坎函数调用的问题。函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。即使在游戏的关键循环中,调用几十个函数也是很常见的。所以函数调用代码的质量,在很大程度上影响着游戏的质量。 还是先说最基本的代码风格问题。首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用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 ); 就说这么多,累了,下次见 |