函數哪裡都有,小的程式一兩個函數,大的程式成百上千個函數。即使在遊戲的關鍵迴圈中,調用幾十個函數也是很常見的。所以函數調用代碼的品質,在很大程度上影響著遊戲的品質。
還是先說最基本的代碼風格問題。首先,對於函數的參數(特別是指標),如果函數內部不會修改其指標的內容,一定要用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 );