寫命中
寫直達(Write Through)
資訊會被同時寫到cache的塊和主存中。這樣做雖然比較慢,但缺少代價小,不需要把整個塊都寫回主存。也不會發生一致性問題。
對於寫直達,多出來%10向主存寫入的儲存指令使得其比其單純向Cache寫入的速度慢上將近10倍。這種速度不一致的問題,不管是在硬體結構還是軟體,有著一條“不管怎麼樣,先試試這樣行不行”的辦法:並行加緩衝。
我們使用寫緩衝(Write Buffer)來解決這個問題,CPU寫入Cache的同時會寫入Write Buffer。緩衝中的內容什麼時候寫入主存交給存控(Memory Controller)來控制,CPU將省下的時間去處理其他事情。
Write Buffer是一個FIFO佇列,一般只有4位元組。
然而當寫操作頻繁的時候,這點容量就不夠用了,容易飽和發生阻塞,就相當於沒加一樣...
解決的辦法可以是再加一級Cache,變成二級快取。什麼,你問為什麼不把兩級Cache和一起,搞那麼多幹什麼?這個二級當然是慢一點的便宜貨,我們們弄這些東西,不就是因為越快東西越貴,買不起嘛土豪!
對於緩衝的問題,還有個關於合併寫對程式效率的影響, 具體可以參考這篇博文。
第二種辦法就是改變策略使用寫回,也就是下面介紹的方法。
寫回(Write Back)
資訊僅僅寫到Cache中的塊。當其被替換時,資訊才會被寫回到主存中。虛擬儲存器系統通常採用寫回策略,因為寫到磁碟的延遲代價太大。
寫回的速度要更快一些,因為不必每次寫操作都訪問主存。但這樣我們如何保證一致性問題呢?我們可以給每行新增一個髒位(dirty bit),這樣我們替換這塊Cache時就可以根據髒位來判斷是否需要寫回主存。如果沒有被“弄髒過”,那麼就不需要寫回主存。
不過對於同一塊Cache中的變數X,他不是太喜歡這個機制。因為它的鄰居Y老是被修改,導致X這個只被讀取的變數老得往記憶體跑,它不想跟Y待在一起了,太累人了。
聆聽了X的心聲,我們有什麼辦法可以幫助它嗎?辦法當然是有的,讓Y這個煩人的傢伙單獨待著就行。下面分別執行兩個程式,排除首次裝入的影響(其實寫一塊也行,對齊的技巧源自Disruptor)
public class Padding {
private static class X {
public long p1,p2,p3,p4,p5,p6,p7;//cache padding
public volatile long x=0L;
public long p9,p10,p11,p12,p13,p14,p15;
//8*8剛好佔滿Cache一行,p9...p15只是為了確保x單獨在一行中,不與其他頻繁修改的變數在一起
}
public static X[] arrX=new X[2];
static {
arrX[0]=new X();
arrX[1]=new X();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
for(long i=0;i<100_000_000L;i++)
arrX[0].x=i;
});
Thread thread2=new Thread(()->{
for(long i=0;i<100_000_000L;i++)
arrX[0].x=i;
});
final long start=System.nanoTime();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println((System.nanoTime()-start)/1_000_000);
}
}
package mytask;
public class NoPadding {
private static class X {
public volatile long x=0L;
}
public static X[] arrX=new X[2];
static {
arrX[0]=new X();
arrX[1]=new X();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
for(long i=0;i<100_000_000L;i++)
arrX[0].x=i;
});
Thread thread2=new Thread(()->{
for(long i=0;i<100_000_000L;i++)
arrX[0].x=i;
});
final long start=System.nanoTime();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println((System.nanoTime()-start)/1_000_000);
}
}
寫不命中
對於寫不命中,有兩者方法:寫分配與非寫分配。前者利用空間區域性性,每次都從主存中讀取一個塊裝入Cache更新相應單元。後者則是直接寫主存單元,不將主存塊裝入Cache。