C優化篇之優化記憶體訪問
目前CPU執行速度遠超過記憶體訪問速度,且從趨勢看這種速度差距還會越拉越大,提高記憶體訪問效率將是軟體優化重要而長期的課題。記憶體訪問優化的一般性措施可大體分兩方面:1)減少記憶體訪問;2)調整程式碼使程式集中順序地訪問記憶體。
一、減少記憶體訪問的措施包括:
a.充分利用暫存器
充分利用暫存器快取資料,是減少記憶體訪問的思路之一。C程式編譯後哪些元素由暫存器儲存,哪些又會放進記憶體,取決於CPU以及對應的編譯器規範。以ARM為例,對於遵循ATPCS規則的編譯器:
1)函式前4個引數放在暫存器裡,超出4個則壓入棧記憶體。
2)區域性變數如果沒有取址操作,或有取址但未賦給其他變數,就會被編譯器優先安排暫存器儲存,如暫存器已佔完,則開始在暫存器和棧之間交換儲存。
不同CPU及編譯器有類似規範,注意下面幾點能更充分地利用暫存器:
1)如果函式引數過多,把多個引數組織成結構體,傳遞結構體指標。在很多平臺上,這樣能減少引數入棧的機率。
2)用register提示編譯器把關鍵變數或迴圈內變數用暫存器快取,register暗示編譯器變數將被頻繁使用,應將其儲存在暫存器中,以加快其儲存速度,但要注意它僅僅是個提示,很多時候編譯器並不鳥它。
3)把某些大函式拆分成小函式,防止因暫存器不足導致區域性變數在棧和暫存器之間反覆存取,類似記憶體和硬碟間的內容交換,浪費時間。大函式內區域性變數多,情況複雜,編譯器無法分析清每個區域性變數的作用範圍,常常做出很多無用的壓棧出棧操作。
4)如某熱點函式內經常訪問全域性變數,可新增一個臨時區域性變數,誘導編譯器將該全域性變數內容讀到暫存器中作為其影子,對暫存器進行相關操作,最後賦回全域性變數,以減少記憶體訪問。如:
long product;
void factorialA(long n)
{
long i;
for(i = 1; i <= n;i++ ){ product *= i; }
}
void factorialB(long n)
{
long i
long x = 1;
for(i = 1; i <= n;i++ ){ x *= i; }
product = x;
}
n較大時,上面兩個函式效能有顯著差別,這就是充分利用暫存器的好處。
5)避免區域性變數取址, 編譯器處理區域性變數時一般先儘量用通用暫存器快取,但如果有區域性變數取址操作,意味著該變數只能放在棧記憶體(通用暫存器沒有記憶體地址概念)。如果該區域性變數在迴圈中多次讀寫,此時也同樣可考慮增加中間變數,用完後再寫回。比如下例改動就能提高整體效率:
void f(int *a);
int g(int a);
int test1(int i)
{
int j;
f(&i);
for(j =0;j<1000;j++)
i += g(i);
return i;
}
修改後
int test2(int i)
{
int temp = i;
f(&temp);
i = temp;
for(j =0;j<1000;j++)
i += g(i);
return i;
}
test2中使用了變數的拷貝temp,把temp的地址傳入函式f(),函式f()退出時再把temp回賦給i,這樣變數i不存在取址操作,編譯器就能把它用暫存器儲存。這裡迴圈內對i有數千次訪問,迴圈體中的i放在暫存器相比放在棧記憶體,效率差別相當大。
b.消除指標鏈
訪問多級結構體成員變數時常要使用指標鏈,如:
typedef struct { int x, y, z; }point;
typedef struct {point *pos, *direct; }obj;
void InitPos(obj *p)
{
p->pos->x = 0;
p->pos->y = 0;
p->pos->z = 0;
}
如果編譯器不能確定p->pos->x不是p->pos的別名,程式碼中每次賦值操作都要重新訪問p->pos(瞭解下restrict)。所以最好是手動把p->pos存到一個區域性變數,改為:
void InitPos(obj *p)
{
point *pos = p->pos;
pos->x = 0;
pos->y = 0;
pos->z = 0;
}
這樣只需一次p->pos記憶體訪問,比之前省了兩次。而且這不是節省兩條普通指令,而是兩條訪問隨機記憶體的指標鏈操作。
二、集中連續訪問記憶體包括:
a.合理安排和調整迴圈次序
迴圈中的記憶體訪問多數都是效能熱點,是連續集中還是斷續分散訪問,很大程度影響系統性能。有時僅僅調整迴圈次序,使分散記憶體訪問變得連續,就能大幅提升效能。如:
for(i=0;i<N;i++)
for(j=0;j<N;j++)
A(j,i) = B(j,i) + C(j,i) * D
變為:
for(j=0;j<N;j++)
for(i=0;i<N;i++)
A(j,i) = B(j,i) + C(j,i) * D
交換後, A, B, C均按其在記憶體中的排列順序依次被訪問,而不是像之前那樣跳躍式訪問。訪問記憶體就象逛街購物,好不容易出來一次,自然要儘量把需要的東西都買回去,否則又要多跑,這中間需要時間代價。
b.使用連續記憶體的資料結構
連結串列結構相比陣列,對記憶體的佔用更少且更靈活,但在記憶體訪問密集型的應用中卻會導致效能的明顯下降,因為訪問連結串列節點是分散隨機地訪問記憶體。與之對應訪問陣列記憶體則相對集中且連續,所以如果對連結串列的隨機訪問成為效能阻礙,不妨考慮用陣列代替連結串列,或者從預分配的大塊連續記憶體上分配連結串列節點,而不是用malloc隨機申請記憶體塊。注意:無論cache或non-cache系統中,把離散記憶體訪問變為連續記憶體訪問都能提高系統性能。至於如何寫出cache-friendly的程式碼則是更高階的主題。
以上只是從記憶體角度出發,C語言級優化的幾個基本著眼點,僅為大家拋磚引玉。