本文講述在編寫C程式碼的常用最佳化辦法,分為I/O篇,記憶體篇,演算法篇,MMX彙編篇。
二.記憶體篇
在上一篇中我們講述了如何最佳化檔案的讀寫,這一篇則主要講述對記憶體操作的最佳化,主要有數組的定址,指標鏈表等,還有一些實用技巧。
I.最佳化數組的定址
在編寫程式時,我們常常使用一個一維數組a[M×N]來類比二維數組a[N][M],這個時候訪問a[]一維數組的時候:我們經常是這樣寫a[j×M+i](對於a[j][i])。這樣寫當然是無可置疑的,但是顯然每個定址語句j×M+i都要進行一次乘法運算。現在再讓我們看看二維數值的定址,說到這裡我們不得不深入到C編譯器在申請二維數組和一維數組的內部細節上――實際在申請二位元組和一維數組,編譯器的處理是不一樣的,申請一個a[N][M]的數組要比申請一個a[M×N]的數組佔用的空間大!二維數組的結構是分為兩部分的:
① 是一個指標數組,儲存的是每一行的起始地址,這也就是為什麼在a[N][M]中,a[j]是一個指標而不是a[j][0]資料的原因。
② 是真正的M×N的連續資料區塊,這解釋了為什麼一個二維數組可以象一維數組那樣定址的原因。(即a[j][i]等同於(a[0])[j×M+i])
清楚了這些,我們就可以知道二維數組要比(類比該二維數組的)一維數組定址效率高。因為a[j][i]的定址僅僅是訪問指標數組得到j行的地址,然後再+i,是沒有乘法運算的!
所以,在處理一維數組的時候,我們常常採用下面的最佳化辦法:(偽碼例子)
int a[M*N];
int *b=a;
for(…)
{
b[…]=…;
…………
b[…]=…;
b+=M;
}
這個是遍曆訪問數組的一個最佳化例子,每次b+=M就使得b更新為下一行的頭指標。當然如果你願意的話,可以自己定義一個數組指標來儲存每一行的起始地址。然後按照二維數組的定址辦法來處理一維數組。不過,在這裡我建議你乾脆就直接申請一個二維數組比較的好。下面是動態申請和釋放一個二維數組的C代碼。
int get_mem2Dint(int ***array2D, int rows, int columns) //h.263原始碼
{
int i;
if((*array2D = (int**)calloc(rows, sizeof(int*))) == NULL) no_mem_exit(1);
if(((*array2D)[0] = (int* )calloc(rows*columns,sizeof(int ))) == NULL) no_mem_exit(1);
for(i=1 ; i<rows ; i++)
(*array2D)[i] = (*array2D)[i-1] + columns ;
return rows*columns*sizeof(int);
}
void free_mem2D(byte **array2D)
{
if (array2D)
{
if (array2D[0])
free (array2D[0]);
else
error ("free_mem2D: trying to free unused memory",100);
free (array2D);
}
else
{
error ("free_mem2D: trying to free unused memory",100);
}
}
順便說一下,如果你的數組定址有一個位移量的話,不要寫為a[x+offset],而應該為 b=a+offset,然後訪問b[x]。
不過,如果你不是處理對速度有特別要求的程式的話,這樣的最佳化也就不必要了。記住,如果編普通程式的話,可讀性和可移值性是第一位的。
II.從負數開始的數組
在編程的時候,你是不是經常要處理邊界問題呢?在處理邊界問題的時候,經常下標是從負數開始的,通常我們的處理是將邊界處理分離出來,單獨用額外的代碼寫。那麼當你知道如何使用從負數開始的數組的時候,邊界處理就方便多了。下面是靜態使用一個從-1開始的數組:
int a[M];
int *pa=a+1;
現在如果你使用pa訪問a的時候就是從-1到M-2了,就是這麼簡單。(如果你動態申請a的話,free(a)可不要free(pa)因為pa不是數組的頭地址)
III.我們需要鏈表嗎
相信大家在學習《資料結構》的時候,對鏈表是相當熟悉了,所以我看有人在編寫一些耗時演算法的時候,也採用了鏈表的形式。這樣編寫當然對記憶體的佔用(似乎)少了,可是速度呢?如果你測試:申請並遍曆10000個元素鏈表的時間與遍曆相同元素的數組的時間,你就會發現時間相差了百倍!(以前測試過一個演算法,用鏈表是1分鐘,用數組是4秒鐘)。所以這裡我的建議是:在編寫耗時大的代碼時,儘可能不要採用鏈表!
其實實際上採用鏈表並不能真正節省記憶體,在編寫很多演算法的時候,我們是知道要佔用多少記憶體的(至少也知道個大概),那麼與其用鏈表一點點的消耗記憶體,不如用數組一步就把記憶體佔用。採用鏈表的形式一定是在元素比較少,或者該部分基本不耗時的情況下。
(我估計鏈表主要慢是慢在它是一步步申請記憶體的,如果能夠象數組一樣分配一個大記憶體塊的話,應該也不怎麼耗時,這個沒有具體測試過。僅僅是猜想 :P)