高併發設計-緩存加速之cpu cache

簡介

本章節主要介紹系統中的緩存使用,對於大多數web服務爲了降低響應時間提升性能,都會大量使用緩存進行加速,除了常見的應用層的業務緩存,在我們系統的周邊還存在着各式各樣的其他緩存,也許你沒有感知或者留意,比如cpu cache、瀏覽器cache、DNS cache、nginx 的http cache、mysql 的 query cache、innodb bufferPool cache、註冊中心路由表cache等等都與我們的系統息息相關,在本節就以CPU cache的一些基本原理爲例進行一些基本介紹。

程序局部性原理

在講解緩存之前,我們先了解一下程序的局部性原理,
所謂局部性原理可以理解成爲在一段時間內被訪問的資源,在不久的將來還會被訪問到,這就是時間局部性原理;
對某個存儲區域中的某個地址進行過訪問,那麼不久的將來還會對這個位置附近的資源有訪問訴求,這是空間局部性原理。
舉例子:在解決時間局部性問題上我們可以使用緩存,來應對頻繁被訪問的熱點數據,在空間局部性問題上CPU cache給我們提供了幫助。有人說CPU cache對我們的系統非常重要,但是對於一名業務開發人員好像也不需要關注,畢竟我們不會直接去操作它。好那麼我們繼續往下看。

投’CPU‘所好,四兩撥千斤

那麼看一段代碼:

public class Main {
    public static void main(String[] args) {
        int[][] input = new int[1000][1000];
        long calcTime2 = Main.method2(input);
        long calcTime1 = Main.method1(input);
        System.out.println("calcTime1: " + calcTime1);
        System.out.println("calcTime2: " + calcTime2);
    }
    private static long method1(int[][] input) {
        long startTime = System.currentTimeMillis();
        int temp  = 1;
        for(int i=0;i<input.length;i++) {
            for(int j=0;j<input[i].length;j++) {
                input[i][j] = temp;
            }
        }
        long endTime = System.currentTimeMillis();
        return endTime-startTime;
    }

    private static  long method2(int[][] input) {
        long startTime = System.currentTimeMillis();
        int temp  = 2;
        for(int i=0;i<input.length;i++) {
            for(int j=0;j<input[i].length;j++) {
                input[j][i] = temp;
            }
        }
        long endTime = System.currentTimeMillis();
        return endTime-startTime;
    }
}

method1是滿足了程序的局部性原理,大概耗時是4~6ms,method2不滿足耗時約6~8ms。
爲什麼那麼明顯的差異呢?
原理大概大家也清楚:對於數組我們會給它分配一段連續的存儲空間,因爲我們順序訪問每個每個元素,那麼第一次訪問某個元素,將元素周邊的定長數據作爲一個緩存行放入cpu cache,那麼後續每次順序訪問一直命中cpu cache,緩存的利用率會很高並且硬件性能要優於主存,這就是性能好的原因。
反觀method2,因爲是隨機訪問,所以被加載進cpu cache的緩存不能被有效使用,第一次可能命中了cache,第二次訪問的元素因爲是不連續的,有可能緩存中沒有,所以需要從內存抓取,將緩存行再次放入cpu cache,沒有有效利用歷史的cache,這也就是慢的原因了。

CPU僞共享,性能影響知多少

大家都知道,正常情況下cpu cacheline可以存放多個變量,比如現在是一個2核CPU,核1的線程A訪問一個int[]數組,並把數組裝入cache line,核2線程B訪問該數組的時候可以直接使用CPUcache查詢,聽起來比較完美,但是如果面臨數據修改的時候比如線程B對數據內容做了修改,則會導致緩存行被標記失效,這樣線程A就不能享受緩存的高效了,所以這種緩存共享的效率意義不大且效率比較低,被叫做’僞共享‘。

解決僞共享:針對僞共享如何解決呢?繼續往下看,知名的高性能無鎖隊列Disruptor使用的是填充方式解決的,CPU每次會加載64位的數據,將共享資源通過填充長度佔用獨立一行,也就是將原來一行數據標記分散成多行,就可以解決局部的變化引起緩存失效的問題,最終還是使用空間換取時間。

關於性能的實驗網上有不少,可以使用原始方式、填充方式(java7及以前)、@Contended註解方式(jdk1.8後)定義一個數組,將一個數組進行任務分片,分給多個線程,每個線程負責特定的元素寫入,部分示例如下:

//存在僞共享
public class SharingObject {
    public volatile long value = 0L;
}
//前後填充解決僞共享
public class SharingObject {
    protected long p1, p2, p3, p4, p5, p6, p7;
    public volatile long value = 0L;
    protected long p9, p10, p11, p12, p13, p14, p15;
}
//1.8以後註解方式解決僞共享,配合-XX:-RestrictContended,java原生支持
@Contended
public class SharingObject {
    public volatile long value = 0L;
}

接下來定義ShaingObject[]數組,多線程操作對應的元素,經過驗證,明顯做過緩存行填充的性能會明顯優於存在僞共享的代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章