注意:本文針對的主要是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 ;
}
運作結果是:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX9kkeNBTR65UMVpXTmZEWjZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TMwIzMyUDMxIzMyATM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
可以看到速度相差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+周期
模運算 幾十上百