天天看點

常用的性能優化技巧

注意:本文針對的主要是c/c++語言,不同語言由于機制不同,會出現不适用的情況。

1.二維數組盡量按行讀取

我們知道二位數組實際上是數組的數組,二維數組的每一低維實際上是一個一維數組,而一維數組在記憶體中的位置是連續的,意味着減少了記憶體尋址的時間,同時便于處理器緩存資料,減少了緩存不命中的幾率。

下面是一段測試代碼來說明這個問題:

#include <iostream>
#include <omp.h>
#include <time.h>
#include <Windows.h>
using namespace std;

const long long int SIZEOFMAT = ;
int mat_a[SIZEOFMAT][SIZEOFMAT];

void creat_mat()
{
    for(int i=;i<SIZEOFMAT;i++)
        for(int j=;j<SIZEOFMAT;j++)
        {
            mat_a[i][j] = j;
        }

}

void readMatByRow()
{
    for(int i=;i<SIZEOFMAT;i++)
    {
        int sum = ;
        for(int j=;j<SIZEOFMAT;j++)
        {
            sum += mat_a[i][j];
        }
    }
}

void readMatBycol()
{
    for (int i = ; i<SIZEOFMAT; i++)
    {
        int sum = ;
        for (int j = ; j<SIZEOFMAT; j++)
        {
            sum += mat_a[j][i];
        }
    }
}


int main()
{
    creat_mat();
    DWORD start, end;
    start = GetTickCount();
    readMatByRow();
    end = GetTickCount();
    cout << "row" << endl << end - start << endl;
    Sleep();
    start = GetTickCount();
    readMatBycol();
    end = GetTickCount();
    cout << "col" << endl << end - start << endl;

    return ;
}
           

運作結果是:

常用的性能優化技巧

可以看到速度相差3倍!

事實上,由于我們的任務是數組相加,我們還可以将循環展開來進一步提高效率,如将按行讀取數組那部分改為:

void readMatByRowWithLoopUnrolling()
{
    for (int i = ; i<SIZEOFMAT; i++)
    {
        int sum = ;
        for (int j = ; j<SIZEOFMAT; j+=)
        {
            sum += mat_a[i][j];
            sum += mat_a[i][j+];
            sum += mat_a[i][j+];
            sum += mat_a[i][j+];
            sum += mat_a[i][j+];
        }
    }
}
           

這是運作結果,LoopUnrolling那行是循環展開後的用時,Loop那行是循環未展開的用時(均為按行讀取矩陣),可以看到速度差距還是相當明顯的。

這個的原理也很容易想到,減少了判斷次數,增加了處理器處理流水線的能力,當然還可以展開外層循環,這個就讀者自己去嘗試了。

常用的性能優化技巧

對于循環來說,可操作性還是比較強,另外也沒有固定的套路,但有一點原則就是盡量不讓寄存器溢出的基礎上盡量充分的運用它,比如有一個循環,循環體的代碼量很大,導緻寄存器溢出,這時候,我們可以把沒有資料依賴的部分分拆循環,用多個循環來執行它,以提高效率,另外我們盡量避免把判斷放在循環體裡,減少分支預測失誤的不利影響。

還有一種騷操作,就是利用條件複制指令移除分支,例如以下一段代碼:

if(a > )
{
    x = a;
}
else
{
    x=b;
}
           

可改為:

另外還有種優化思路,那就是定義數組時用short int, 因為我們發現數組的值都沒有超過short int的範圍,不過在這個情境下,這樣對效率的提升比較有限,然而對空間的優化還是相當明顯的。

2.

使用條件編譯,由于宏條件在編譯時已經确定,可以幫助編譯器直接忽略不成立的分支,提高運作效率,不過這也有一個問題,就是隻能使用多個程式編譯。

3.對于編譯器自身來說,選擇合理的編譯優化選項(比如 cl的od/o1/o2/ox),另外比如指定指令集(我這部分還需要學習,過幾天開個部落格專門說這個),再是降低編譯器的優化難度,比如減少全局變量的數量(不過這和有些程式設計原則沖突,需要合理的使用),以及避免存儲器别名,下面我們詳細的說一說存儲器别名的問題,看下面一段代碼:

int f(int *a,int *b)
{
    *a += *b;
    *a += *b;
}
           

假如我們是編譯器,我們可以嘗試将函數簡化成:

int f(int *a,int *b)
{   
    int temp = *b;
    *a += *temp;
}
           

上面這段代碼,如果a,b之間存在存儲器别名,即a,b指向的是同一段記憶體,那麼很明顯結果是錯的,而應該變為如下代碼:

int f(int *a, int *b)
{
    int temp= *b;
    *a=  * temp;
}
           

是以編譯器為了保險起見便不再優化,但是我們可以通過restrict手工指定指針不是存儲器别名,在vs中使用宏RESTRICTED_POINTER來定義,如下:

int f(int * RESTRICTED_POINTER a, int * RESTRICTED_POINTER b)
{
    *a += *b;
    *a += *b;
}
           

以提供給編譯器優化空間。

4.

适當的使用inline(内聯函數),但是注意它會引起空間上的開銷。

注意在X86的處理器上,函數的參數優先存入寄存器中,如果你的函數參數是一個巨大的結構體,請使用結構體的指針來作為參數傳遞而不是整個結構體(這個的前提是你隻調用這個結構體的一部分,如果當然如果你需要調用整個結構體,那麼用指針傳遞會造成比較大的開銷)

說到結構體,我們也可以采用一些方式來對我們結構體進行優化,比如,對結構體進行按位元組對齊,在vs中指令為:#pragma pack(push,size)

用#pragma pack(pop)來還原預設

另外還有一個小技巧,聲明結構體時,大資料類型在前,小資料類型在後。

5.

性能比較:

位運算 1周期

乘法 3周期

除法 10+周期

模運算 幾十上百

繼續閱讀