現代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).如果我們能在緩沖區傳輸到緩存之前將其盡可能填滿,這樣的效果就會提高各級傳輸總線的效率,以提高程序性能。

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

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

#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底層特性對程序員並不是透明的。程序的稍微改變會帶來顯著的性能提升。對於存儲密集型的程序,更應當考慮到此到特性。

希望這篇文章能該大家帶來一些幫助,也能可做性能優化的同事帶來參考。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM