Cache寫策略(Cache一致性問題與騷操作)

AD_milk發表於2020-07-04

寫命中

寫直達(Write Through)

資訊會被同時寫到cache的塊和主存中。這樣做雖然比較慢,但缺少代價小,不需要把整個塊都寫回主存。也不會發生一致性問題。

對於寫直達,多出來%10向主存寫入的儲存指令使得其比其單純向Cache寫入的速度慢上將近10倍。這種速度不一致的問題,不管是在硬體結構還是軟體,有著一條“不管怎麼樣,先試試這樣行不行”的辦法:並行加緩衝。

Write Buffer

我們使用寫緩衝(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。

相關文章