1. 程式人生 > >C優化篇之優化記憶體訪問

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隨機申請記憶體塊。注意:無論cachenon-cache系統中,把離散記憶體訪問變為連續記憶體訪問都能提高系統性能。至於如何寫出cache-friendly的程式碼則是更高階的主題。

以上只是從記憶體角度出發,C語言級優化的幾個基本著眼點,僅為大家拋磚引玉。