偽共享 FalseSharing (CacheLine,MESI) 淺析以及解決方案


起因

在閱讀百度的發號器 uid-generator 源碼的過程中,發現了一段很奇怪的代碼:

/**
 * Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
 * 
 * The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
 * 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
 * 
 * @author yutianbao
 */
public class PaddedAtomicLong extends AtomicLong {
    private static final long serialVersionUID = -3415778863941386253L;

    /** Padded 6 long (48 bytes) */
    public volatile long p1, p2, p3, p4, p5, p6 = 7L;

    /**
     * Constructors from {@link AtomicLong}
     */
    public PaddedAtomicLong() {
        super();
    }

    public PaddedAtomicLong(long initialValue) {
        super(initialValue);
    }

}

這里面有6個看上去毫無作用的volatile long變量(標紅)。如果這是我自己寫的代碼,我肯定會認為是我自己手抖寫多了。

但是作為百度的發號器,開源了這么久,如果是手抖早被fix了。肯定還是有深意的。於是閱讀了一些類注釋,看到了這句話:

to prevent the FalseSharing problem

果然,這幾個變量不是毫無作用的,是為了解決FalseSharing問題。

但是轉念一想,我好像不知道什么是FalseSharing?解決了一個問題,又陷入了另一個更大的問題。

於是就上網查了很多資料,閱讀了很多博客,算是對FalseSharing有了一個初步的了解。在這里寫出來也為了希望能幫到有同樣困惑的人。

背景知識

要說清楚FalseSharing,不是一兩句話能做到的事,有一些必須了解的背景知識需要補充一下。

 

 

 

計算機存儲架構

 

上圖展示的是不同層級的硬件和cpu之間的交互延遲。越靠近CPU,速度越快。

計算機運行時,CPU是執行指令的地方,而指令會需要一些數據的讀寫。程序的運行時數據都是存放在主存的,而主存又特別慢(相對),所以為了解決CPU和主存之間的速度差異,現代計算機都引入了高速緩存(L1L2L3)。

 

現代計算機對緩存/內存的設計一般如下:

 

L1和L2由CPU的每個核心獨享,而L3則被整個CPU里所有核心共享(僅指單CPU架構)。

CPU訪問數據時,按照先去L1,查不到去L2,再L3->主存的順序來查找。

 

Cache Line

 

在上述CPU和緩存的數據交換過程中,並不是以字節為單位的。而是每次都會以Cache Line為單位來進行存取。

Cache Line其實就是一段固定大小的內存空間,一般為64字節。

 

MESI

這個東西研究過 volatile的同學可能會比較熟悉,這個就是各個告訴緩存之間的一個一致性協議。

因為L1 L2是每個核心自己使用,而不同核心又可能涉及共享變量問題,所以各個高速緩存間勢必會有一致性的問題。MESI就是解決這些問題的一種方式。

MESI大致原理如下圖:

 

 

 我這里就摘抄一下網上搜到的解釋:

在MESI協議中,每個Cache line有4個狀態,可用2個bit表示,它們分別是:
M(Modified):這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中;
E(Exclusive):這行數據有效,數據和內存中的數據一致,數據只存在於本Cache中;
S(Shared):這行數據有效,數據和內存中的數據一致,數據存在於很多Cache中;
I(Invalid):這行數據無效。

 

通俗一點說,就是如果Core0和Core1都在使用一個共享變量變量A,則0,1都會在自己的Cache里有一份A的副本,分布在不同的CacheLine。

如果大家都沒有修改A,則Core0和Core1里變量A所在的Cache Line的狀態都是S。

如果Core0修改了A的值,則此時Core0的Cache Line變為M,Core1 的Cache Line變為I。

 

這樣CPU就可以通過CacheLine的狀態,來決定是刪除緩存,還是直接讀取什么的。

 

偽共享

背景知識介紹完畢了,這樣再說偽共享就不會顯得太難以理解了。

 

先說一個場景:

你的代碼里需要使用一個volatile的Bool變量,當做多線程行為的一個開關:

static volatile boolean flag = true;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Integer count = 0;
                while (flag) {
                    ++count;
                    System.out.println(Thread.currentThread().getName() + ":" + count);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;

        }).start();
    }

這段代碼會聲明一個flag為true,然后有10個工作線程會在flag為true時沒100ms對count做個自增操作,然后輸出。當flag為false時,就會結束線程。

還有一個線程A,會在1000ms后將flag置為false。

這里就是volatile的一個經典用法,可以保證多個線程對flag的可見性,不會因為線程A修改了flag的值,但是工作線程讀取到的不是最新值而額外執行一些工作。

 

這段代碼看起來是沒有任何問題的,實際上跑起來也沒有問題。

但是結合之前的背景知識,考慮一下flag所在的cache line,肯定還會有其他的變量(cache line 64字節,bool無法完整填充一個CacheLine)。

如果flag所在的CacheLine里還有一個頻繁修改的共享變量,這時會發生什么?

很簡單,就是flag所在的CacheLine被頻繁置為不可用,需要清除緩存重新讀取。flag在工作狀態並沒有被修改,但是仍然會被其他頻繁修改的共享變量所影響。

這樣就會帶來一個問題,即使flag並沒有被修改,但我們的工作線程很多時間都等於是在主存中讀取flag的值,這樣在高並發時會帶來很大的效率問題。

 

以上就是所謂的 “FalseSharing” 問題。

 

 

解決辦法

FalseSharing對於普通業務應用,基本沒什么實際影響。但是對於很多超高並發的中間件(例如發號器),可能就會帶來一定的性能瓶頸。所以這類項目都是需要關注這個問題的。

出現原因已經說清楚了,那么該如何解決呢?

其實答案就在文章的開頭,那6個看上去沒有任何含義的volatile long變量,就是用來解決這個問題的。

The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)

 這行注釋就說明了這6個變量是如何解決FalseSharing問題的:

CacheLine一般是64字節,64 = 8(對象本身的屬性信息)+ 6*8(long占用8個字節) + 8 (AtomicLong本身帶有一個long) 。

寫了這6個看着無效的變量后,PaddedAtomicLong就會占用64個字節,正好填滿一個CacheLine,這樣就會被獨自分配到一個CacheLine,這樣就不存在FalseSharing問題了。

 

需要注意的是本來AtomicLong僅占用不到20字節,但是為了解決FalseSharing做了填充之后就占用64字節了,這樣就會導致空間會膨脹很多。所以即使用的時候也要做好取舍。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM