現代處理器由指令控制單元(ICU)和執行單元(EU)組成,ICU負責取指、譯碼,EU負責指令執行,爲了並行處理ICU會預取指令,因此EU執行的指令一般ICU在前期就取出譯碼,對於含有分支的代碼就包含多條取指路徑,ICU在分支路徑採用了分支預測技術,既預先選擇一條分支取指譯碼,若是後期發現預測失敗則須要從新取指,這意味着浪費了幾個時鐘週期。分支預測算法一般基於之前運行的結果來做爲下次預測的依據。linux
1. 優化分支預測:算法
因爲預測算法一般基於之前的結果,若是咱們的代碼分支條件的判斷是有規律的,則有助於分支預測的準確性,以下例所示:數組
#include <stdlib.h> #include <time.h> #include <stdio.h> #define arraySize 10000000 int array[arraySize] = {0}; int searchInArray(int value) { int sum = 0; for(int i=0; i<arraySize;i++) { if(value > array[i]) { sum+=array[i]; } } return sum; } int main() { clock_t start; clock_t end; int sum = 0; //有序數組查找 for(int i=0; i<arraySize; i++) { array[i] = i; } start = clock(); sum += searchInArray(500); end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); //無序數組查找 for(int i=0; i<arraySize; i++) { array[i] = rand()%1000; } start = clock(); sum += searchInArray(500); end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); return 0; }
在這個例子中咱們對數組中大於指定值的元素求和,分別對有序數組和無序數組進行測試,無序數組的時間消耗基本是有序數組的三倍左右,這裏是因爲if(value > array[i])分支預測中,若是數組已經有序,基於之前的預測結果後續分支預測基本都會成功,若是是無序數組則後續的預測沒有依據。這裏要注意對於編譯期能夠肯定值的無序數組,一般編譯器會進行優化。在linux中咱們一般可使用likely、unlikely來優化指令的預測,likely表示很可能發生, unlikely表示不怎麼發生,一樣也是基於分支預測的原理。例如在入參指針判斷上可使用if(unlilely(NULL == ptr))來優化。緩存
2. 避免分支預測:性能優化
在代碼層面實現一樣的功能,也能夠經過使用不一樣的代碼編寫方式來避免一些分支預測,一個例子就是雙重for循環的問題,入下例所示:ide
int main() { clock_t start; clock_t end; int i = 0; int j = 0; start = clock(); for(i=0; i<100000000; i++) { for(j=0; j<1; j++); } end = clock(); printf("Time Expense is %d\n",end-start); start = clock(); for(i=0; i<1; i++) { for(j=0; j<100000000; j++); } end = clock(); printf("Time Expense is %d\n",end-start); return 0; }
對於上面的例子的雙重循環,當外層循環次數大於內層循環時,耗時基本是後者的兩倍,這是因爲對於for(i=0; i<m; i++)這種分支預測,存在預測失敗的可能,咱們假設一個for循環分支預測失敗x次,那麼若是外層循環m次內層循環n次,整個雙重循環的預測失敗次數是x*m+x次。能夠看到外層循環次數越多其分支預測的失敗次數也越多。性能
程序運行期間的數據來自存儲器,現代計算的存儲器採用分層設計,越上層靠近CPU的存儲設備存取速度越快,上層做爲下層數據的緩存是其的一個子集,從上層到下層一般多是寄存器->cache->主存->磁盤。這使得咱們在編寫代碼的時候能夠經過考慮數據存放的位置來優化數據的存取性能。入下例所示:測試
void Summation1(int* pSum, int size) { for(int i=0; i<size-1; i++) { pSum[i+1] += pSum[i]; } return; } void Summation2(int* pSum, int size) { int temp; temp = 0; for(int i=0; i<size-1; i++) { temp += pSum[i]; } pSum[size-1] = temp; return; } int sum1[5000000] = {1,2,3,4,5,6,7,8,9,0}; int sum2[5000000] = {1,2,3,4,5,6,7,8,9,0}; int main() { clock_t start; clock_t end; int i = 0; start = clock(); Summation1(sum1,5000000); end = clock(); printf("Time Expense is %d\n",end-start); start = clock(); Summation2(sum2,5000000); end = clock(); printf("Time Expense is %d\n",end-start); return 0; }
對於一樣的求和功能實現,Summation2比Summation1性能優化三倍左右,這是因爲Summation1中咱們是直接對指針操做,也就是直接對cache進行讀寫,每次循環要進行兩次讀一次寫cache,而在Summation2中咱們把經過使用局部變量把對cache的讀寫轉換爲寄存器的讀寫,每次循環只有一次cache讀和一次寄存器的讀寫。優化
因爲計算機存儲的分層設計,程序在訪問數據的時候先在上層的較快的存儲設備中查找,沒有命中再依次到下層存儲設備中查找,下層命中的數據將傳遞給上層,不一樣層之間以塊大小進行傳輸,塊的大小由層間約定。以下圖所示:spa
程序在查找地址1的數據時上層存儲設備沒有緩存沒有命中,而後在下層存儲中找到該數據,下層存儲將地址1所在塊(假設大小爲4)的數據所有傳送給上層緩存。上層緩存一次緩存一整塊數據。基於這個原理就能夠引入了程序設計中局部性原理,咱們在設計程序的時候應該訪問最近引用數據相鄰的數據(空間局部性)和引用最近引用過的數據(時間局部性)。上圖中咱們若是引用二、三、4都會直接在上層命中,一樣若是咱們一直引用1也會一直在上層命中。一個常見的例子就是二維數組的訪問問題,入下例所示:
int array1[50000][1000]; int array2[50000][1000]; int main() { clock_t start; clock_t end; int i = 0; int j = 0; int sum = 0; start = clock(); for(i=0; i< 50000; i++) { for(j=0; j<1000; j++) { sum+=array1[i][j]; } } end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); start = clock(); for(i=0; i< 1000; i++) { for(j=0; j<50000; j++) { sum+=array2[j][i]; } } end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); return 0; }
例子中的兩種循環訪問方式都是對二維數組進行循環求和,但前者的效率要比後者高出二十倍左右,前者對數組的訪問方式以下左圖,後者對數組的訪問方式以下右圖,根據程序的局部性訪問原理咱們能夠很容易的看到後者的訪問方式會致使上層緩存常常不命中。
這裏能夠看到前面的循環雖然分支預測失敗的次數可能更多,但效率仍是要好不少,說明存儲器的訪問速度是制約程序性能的主要瓶頸。
以上代碼的都是在筆者的機器上測試,不一樣的環境可能存在差別。
轉載請註明原始出處:http://www.cnblogs.com/chencheng/p/34002