常用的性能優化技巧

注意:本文針對的主要是c/c++語言,不同語言由於機制不同,會出現不適用的情況。
1.二維數組儘量按行讀取
我們知道二位數組實際上是數組的數組,二維數組的每一低維實際上是一個一維數組,而一維數組在內存中的位置是連續的,意味着減少了內存尋址的時間,同時便於處理器緩存數據,減少了緩存不命中的機率。

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

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

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

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

}

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

void readMatBycol()
{
    for (int i = 0; i<SIZEOFMAT; i++)
    {
        int sum = 0;
        for (int j = 0; 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(10);
    start = GetTickCount();
    readMatBycol();
    end = GetTickCount();
    cout << "col" << endl << end - start << endl;

    return 0;
}

運行結果是:
這裏寫圖片描述
可以看到速度相差3倍!

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

void readMatByRowWithLoopUnrolling()
{
    for (int i = 0; i<SIZEOFMAT; i++)
    {
        int sum = 0;
        for (int j = 0; j<SIZEOFMAT; j+=5)
        {
            sum += mat_a[i][j];
            sum += mat_a[i][j+1];
            sum += mat_a[i][j+2];
            sum += mat_a[i][j+3];
            sum += mat_a[i][j+4];
        }
    }
}

這是運行結果,LoopUnrolling那行是循環展開後的用時,Loop那行是循環未展開的用時(均爲按行讀取矩陣),可以看到速度差距還是相當明顯的。
這個的原理也很容易想到,減少了判斷次數,增加了處理器處理流水線的能力,當然還可以展開外層循環,這個就讀者自己去嘗試了。
這裏寫圖片描述
對於循環來說,可操作性還是比較強,另外也沒有固定的套路,但有一點原則就是儘量不讓寄存器溢出的基礎上儘量充分的運用它,比如有一個循環,循環體的代碼量很大,導致寄存器溢出,這時候,我們可以把沒有數據依賴的部分分拆循環,用多個循環來執行它,以提高效率,另外我們儘量避免把判斷放在循環體裏,減少分支預測失誤的不利影響。
還有一種騷操作,就是利用條件複製指令移除分支,例如以下一段代碼:

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

可改爲:

x = (a>0 ? a : 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 += 2*temp;
}

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

int f(int *a, int *b)
{
    int temp= *b;
    *a= 4 * 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+週期
模運算 幾十上百

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章