對於現代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底層特性對程序員並非透明的。程序的稍微改變會帶來顯著的性能提高。對於存儲密集型的程序,更應當考慮到此到特性。
但願這篇文章能該你們帶來一些幫助,也能可作性能優化的同事帶來參考。