注意:本文針對的主要是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+週期
模運算 幾十上百