合並寫(write combining)
現代CPU采用了大量的技術來抵消內存訪問帶來的延遲。讀寫內存數據期間,CPU能執行成百上千條指令。
多級SRAM緩存是減小這種延遲帶來的影響的主要手段。此外,SMP係統采用消息傳遞協議來實現緩存之間的一致性。遺憾的是,現代的CPU實在是太快了,即使是使用了緩存,有時也無法跟上CPU的速度。因此,為了進一步減小延遲的影響,一些鮮為人知的緩衝區派上了用場。
本文將探討“合並寫存儲緩衝區(write combining store buffers)”,以及如何寫出有效利用它們的代碼。
CPU緩存是一種高效的非鏈式結構的hash map,每個桶(bucket)通常是64個字節。這就是一個“緩存行(cache line)”。緩存行是內存交換的實際單位。例如,主存中地址A會映射到一個給定的緩存行C。
如果CPU需要訪問的地址hash後的行尚不在緩存中,那麼緩存中對應位置的緩存行會被清除,以便載入新的行。例如,如果我們有兩個地址,通過hash算法hash到同一緩存行,那麼新的值會覆蓋老的值。
當CPU執行存儲指令(store)時,它會嚐試將數據寫到離CPU最近的L1緩存。如果此時出現緩存未命中,CPU會訪問下一級緩存。此時,無論是英特爾還是許多其它廠商的CPU都會使用一種稱為“合並寫(write combining)”的技術。
在請求L2緩存行的所有權尚未完成時,待存儲的數據被寫到處理器自身的眾多跟緩存行一樣大小的存儲緩衝區之一。這些芯片上的緩衝區允許CPU在緩存子係統準備好接收和處理數據時繼續執行指令。當數據不在任何其它級別的緩存中時,將獲得最大的優勢。
當後續的寫操作需要修改相同的緩存行時,這些緩衝區變得非常有趣。在將後續的寫操作提交到L2緩存之前,可以進行緩衝區寫合並。 這些64字節的緩衝區維護了一個64位的字段,每更新一個字節就會設置對應的位,來表示將緩衝區交換到外部緩存時哪些數據是有效的。
也許你要問,如果程序要讀取已被寫入緩衝區的某些數據,會怎麼樣?我們的硬件工程師已經考慮到了這點,在讀取緩存之前會先去讀取緩衝區的。
這一切對我們的程序意味著什麼?
如果我們能在緩衝區被傳輸到外部緩存之前將其填滿,那麼將大大提高各級傳輸總線的效率。如何才能做到這一點呢?好的程序將大部分時間花在循環處理任務上。
這些緩衝區的數量是有限的,且隨CPU模型而異。例如在Intel CPU中,同一時刻隻能拿到4個。這意味著,在一個循環中,你不應該同時寫超過4個不同的內存位置,否則你將不能享受到合並寫(write combining)的好處。
代碼如下:
public final class WriteCombining { private static final int ITERATIONS = Integer.MAX_VALUE; private static final int ITEMS = 1 << 24; private static final int MASK = ITEMS - 1; private static final byte[] arrayA = new byte[ITEMS]; private static final byte[] arrayB = new byte[ITEMS]; private static final byte[] arrayC = new byte[ITEMS]; private static final byte[] arrayD = new byte[ITEMS]; private static final byte[] arrayE = new byte[ITEMS]; private static final byte[] arrayF = new byte[ITEMS]; public static void main(final String[] args) { for (int i = 1; i <= 3; i++) { out.println(i + " SingleLoop duration (ns) = " + runCaseOne()); out.println(i + " SplitLoop duration (ns) = " + runCaseTwo()); } int result = arrayA[1] + arrayB[2] + arrayC[3] + arrayD[4] + arrayE[5] + arrayF[6]; out.println("result = " + result); } public static long runCaseOne() { long start = System.nanoTime(); int i = ITERATIONS; while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayA[slot] = b; arrayB[slot] = b; arrayC[slot] = b; arrayD[slot] = b; arrayE[slot] = b; arrayF[slot] = b; } return System.nanoTime() - start; } public static long runCaseTwo() { long start = System.nanoTime(); int i = ITERATIONS; while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayA[slot] = b; arrayB[slot] = b; arrayC[slot] = b; } i = ITERATIONS; while (--i != 0) { int slot = i & MASK; byte b = (byte) i; arrayD[slot] = b; arrayE[slot] = b; arrayF[slot] = b; } return System.nanoTime() - start; } }
這個程序在我的Windows 7 64位英特爾酷睿i7860@2.8 GHz係統上產生的輸出如下:
1 SingleLoop duration (ns) = 14019753545 1 SplitLoop duration (ns) = 8972368661 2 SingleLoop duration (ns) = 14162455066 2 SplitLoop duration (ns) = 8887610558 3 SingleLoop duration (ns) = 13800914725 3 SplitLoop duration (ns) = 7271752889
上麵的例子說明:如果在一個循環中修改6個數組位置(內存地址),程序的運行時間明顯長於將任務拆分的方式,即,先寫前3個位置,再修改後3個位置。
通過拆分循環,我們做了更多的工作,但程序花費的時間更少!歡迎利用神奇的“合並寫(write combining)”。通過使用CPU架構的知識,正確的填充這些緩衝區,我們可以利用底層硬件加速我們的程序。
不要忘了超線程(hyper-threading),可能會有2個線程競爭同一個核的緩衝區。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 17:31:47