現代cpu的合併寫技術對程序的影響

   對於現代cpu而言,性能瓶頸則是對於內存的訪問。cpu的速度每每都比主存的高至少兩個數量級。所以cpu都引入了L1_cache與L2_cache,更加高端的cpu還加入了L3_cache.很顯然,這個技術引發了下一個問題:程序員

         若是一個cpu在執行的時候須要訪問的內存都不在cache中,cpu必需要經過內存總線到主存中取,那麼在數據返回到cpu這段時間內(這段時間大體爲cpu執行成百上千條指令的時間,至少兩個數據量級)幹什麼呢? 答案是cpu會繼續執行其餘的符合條件的指令。好比cpu有一個指令序列 指令1  指令2  指令3 …, 在指令1時須要訪問主存,在數據返回前cpu會繼續後續的和指令1在邏輯關係上沒有依賴的」獨立指令」,cpu通常是依賴指令間的內存引用關係來判斷的指令間的」獨立關係」,具體細節可參見各cpu的文檔。這也是致使cpu亂序執行指令的根源之一。緩存

         以上方案是cpu對於讀取數據延遲所作的性能補救的辦法。對於寫數據則會顯得更加複雜一點:性能優化

         當cpu執行存儲指令時,它會首先試圖將數據寫到離cpu最近的L1_cache, 若是此時cpu出現L1未命中,則會訪問下一級緩存。速度上L1_cache基本能和cpu持平,其餘的均明顯低於cpu,L2_cache的速度大約比cpu慢20-30倍,並且還存在L2_cache不命中的狀況,又須要更多的週期去主存讀取。其實在L1_cache未命中之後,cpu就會使用一個另外的緩衝區,叫作合併寫存儲緩衝區。這一技術稱爲合併寫入技術。在請求L2_cache緩存行的全部權還沒有完成時,cpu會把待寫入的數據寫入到合併寫存儲緩衝區,該緩衝區大小和一個cache line大小,通常都是64字節。這個緩衝區容許cpu在寫入或者讀取該緩衝區數據的同時繼續執行其餘指令,這就緩解了cpu寫數據時cache miss時的性能影響。函數

當後續的寫操做須要修改相同的緩存行時,這些緩衝區變得很是有趣。在將後續的寫操做提交到L2緩存以前,能夠進行緩衝區寫合併。 這些64字節的緩衝區維護了一個64位的字段,每更新一個字節就會設置對應的位,來表示將緩衝區交換到外部緩存時哪些數據是有效的。固然,若是程序讀取已被寫入到該緩衝區的某些數據,那麼在讀取緩存數據以前會先去讀取本緩衝區的。性能

通過上述步驟後,緩衝區的數據仍是會在某個延時的時刻更新到外部的緩存(L2_cache).若是咱們能在緩衝區傳輸到緩存以前將其儘量填滿,這樣的效果就會提升各級傳輸總線的效率,以提升程序性能。測試

從下面這個具體的例子來看吧:優化

下面一段測試代碼,從代碼自己就能看出它的基本邏輯。blog

#include <unistd.h>內存

#include <stdio.h>文檔

#include <sys/time.h>

#include <stdlib.h>

#include <limits.h>

 

static const int iterations = INT_MAX;

static const int items = 1<<24;

static int mask;

 

static int arrayA[1<<24];

static int arrayB[1<<24];

static int arrayC[1<<24];

static int arrayD[1<<24];

static int arrayE[1<<24];

static int arrayF[1<<24];

static int arrayG[1<<24];

static int arrayH[1<<24];

 

 

double run_one_case_for_8()

{

         double start_time;

         double end_time;

         struct timeval start;

         struct timeval end;

 

         int i = iterations;

         gettimeofday(&start, NULL);

        

         while(--i != 0)

         {

                  int slot = i & mask;

                  int value = i;

                  arrayA[slot] = value;

                  arrayB[slot] = value;

                  arrayC[slot] = value;

                  arrayD[slot] = value;

                  arrayE[slot] = value;

                  arrayF[slot] = value;

                  arrayG[slot] = value;

                  arrayH[slot] = value;

                 

         }

        

         gettimeofday(&end, NULL);

         start_time = (double)start.tv_sec + (double)start.tv_usec/1000000.0;

         end_time = (double)end.tv_sec + (double)end.tv_usec/1000000.0;

         return end_time - start_time;

}

 

double run_two_case_for_4()

{

         double start_time;

         double end_time;

         struct timeval start;

         struct timeval end;

 

         int i = iterations;

         gettimeofday(&start, NULL);

        

         while(--i != 0)

         {

                  int slot = i & mask;

                  int value = i;

                  arrayA[slot] = value;

                  arrayB[slot] = value;

                  arrayC[slot] = value;

                  arrayD[slot] = value;

         }

        

         i = iterations;

         while(--i != 0)

         {

                  int slot = i & mask;

                  int value = i;

                  arrayG[slot] = value;

                  arrayE[slot] = value;

                  arrayF[slot] = value;

                  arrayH[slot] = value;

         }

        

         gettimeofday(&end, NULL);

         start_time = (double)start.tv_sec + (double)start.tv_usec/1000000.0;

         end_time = (double)end.tv_sec + (double)end.tv_usec/1000000.0;

         return end_time - start_time;

        

}

 

int main()

{

         mask = items -1;

         int i;

         printf("test begin---->\n");

        

         for(i=0;i<3;i++)

         {

                  printf(" %d, run_one_case_for_8: %lf\n", i, run_one_case_for_8());

                  printf(" %d, run_two_case_for_4: %lf\n", i, run_two_case_for_4());

         }

         printf("test end");

         return 0;

}

 

相信不少人會認爲run_two_case_for_4 的運行時間確定要比run_one_case_for_8的長,由於至少前者多了一遍循環的i++操做。可是事實卻不是這樣:下面是運行的截圖:

 

測試環境: fedora 20 64bits, 4G DDR3內存,CPU:Inter® Core™ i7-3610QM cpu @2.30GHZ.

結果是使人吃驚的,他們的性能差距竟然達到了1倍,太神奇了。

 

原理:上面提到的合併寫存入緩衝區離cpu很近,容量爲64字節,很小了,估計很貴。數量也是有限的,我這款cpu它的個數爲4。個數時依賴cpu模型的,intel的cpu在同一時刻只能拿到4個。

所以,run_one_case_for_8函數中連續寫入8個不一樣位置的內存,那麼當4個數據寫滿了合併寫緩衝時,cpu就要等待合併寫緩衝區更新到L2cache中,所以cpu就被強制暫停了。然而在run_two_case_for_4函數中是每次寫入4個不一樣位置的內存,能夠很好的利用合併寫緩衝區,因合併寫緩衝區滿到引發的cpu暫停的次數會大大減小,固然若是每次寫入的內存位置數目小於4,也是同樣的。雖然多了一次循環的i++操做(實際上你可能會問,i++也是會寫入內存的啊,其實i這個變量保存在了寄存器上), 可是它們之間的性能差距依然很是大。

從上面的例子能夠看出,這些cpu底層特性對程序員並非透明的。程序的稍微改變會帶來顯著的性能提高。對於存儲密集型的程序,更應當考慮到此到特性。

但願這篇文章能該你們帶來一些幫助,也能可作性能優化的同事帶來參考。

相關文章
相關標籤/搜索