1. 程式人生 > >雜談 什麼是偽共享(false sharing)?

雜談 什麼是偽共享(false sharing)?

問題

(1)什麼是 CPU 快取行?

(2)什麼是記憶體屏障?

(3)什麼是偽共享?

(4)如何避免偽共享?

CPU快取架構

CPU 是計算機的心臟,所有運算和程式最終都要由它來執行。

主記憶體(RAM)是資料存放的地方,CPU 和主記憶體之間有好幾級快取,因為即使直接訪問主記憶體也是非常慢的。

如果對一塊資料做相同的運算多次,那麼在執行運算的時候把它載入到離 CPU 很近的地方就有意義了,比如一個迴圈計數,你不想每次迴圈都跑到主記憶體去取這個資料來增長它吧。

越靠近 CPU 的快取越快也越小。

所以 L1 快取很小但很快,並且緊靠著在使用它的 CPU 核心。

L2 大一些,也慢一些,並且仍然只能被一個單獨的 CPU 核使用。

L3 在現代多核機器中更普遍,仍然更大,更慢,並且被單個插槽上的所有 CPU 核共享。

最後,主存儲存著程式執行的所有資料,它更大,更慢,由全部插槽上的所有 CPU 核共享。

當 CPU 執行運算的時候,它先去 L1 查詢所需的資料,再去 L2,然後是 L3,最後如果這些快取中都沒有,所需的資料就要去主記憶體拿。

走得越遠,運算耗費的時間就越長。

所以如果進行一些很頻繁的運算,要確保資料在 L1 快取中。

CPU快取行

快取是由快取行組成的,通常是 64 位元組(常用處理器的快取行是 64 位元組的,比較舊的處理器快取行是 32 位元組),並且它有效地引用主記憶體中的一塊地址。

一個 Java 的 long 型別是 8 位元組,因此在一個快取行中可以存 8 個 long 型別的變數。

在程式執行的過程中,快取每次更新都從主記憶體中載入連續的 64 個位元組。因此,如果訪問一個 long 型別的陣列時,當陣列中的一個值被載入到快取中時,另外 7 個元素也會被載入到快取中。

但是,如果使用的資料結構中的項在記憶體中不是彼此相鄰的,比如連結串列,那麼將得不到免費快取載入帶來的好處。

不過,這種免費載入也有一個壞處。設想如果我們有個 long 型別的變數 a,它不是陣列的一部分,而是一個單獨的變數,並且還有另外一個 long 型別的變數 b 緊挨著它,那麼當載入 a 的時候將免費載入 b。

看起來似乎沒有什麼毛病,但是如果一個 CPU 核心的執行緒在對 a 進行修改,另一個 CPU 核心的執行緒卻在對 b 進行讀取。

當前者修改 a 時,會把 a 和 b 同時載入到前者核心的快取行中,更新完 a 後其它所有包含 a 的快取行都將失效,因為其它快取中的 a 不是最新值了。

而當後者讀取 b 時,發現這個快取行已經失效了,需要從主記憶體中重新載入。

請記住,我們的快取都是以快取行作為一個單位來處理的,所以失效 a 的快取的同時,也會把 b 失效,反之亦然。

這樣就出現了一個問題,b 和 a 完全不相干,每次卻要因為 a 的更新需要從主記憶體重新讀取,它被快取未命中給拖慢了。

這就是傳說中的偽共享。

偽共享

好了,上面介紹完CPU的快取架構及快取行機制,下面進入我們的正題——偽共享。

當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。

我們來看看下面這個例子,充分說明了偽共享是怎麼回事。

public class FalseSharingTest {

    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }

    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}

class Pointer {
    volatile long x;
    volatile long y;
}

這個例子中,我們聲明瞭一個 Pointer 的類,它包含 x 和 y 兩個變數(必須宣告為volatile,保證可見性,關於記憶體屏障的東西我們後面再講),一個執行緒對 x 進行自增1億次,一個執行緒對 y 進行自增1億次。

可以看到,x 和 y 完全沒有任何關係,但是更新 x 的時候會把其它包含 x 的快取行失效,同時也就失效了 y,執行這段程式輸出的時間為3890ms

避免偽共享

偽共享的原理我們知道了,一個快取行是 64 個位元組,一個 long 型別是 8 個位元組,所以避免偽共享也很簡單,筆者總結了下大概有以下三種方式:

(1)在兩個 long 型別的變數之間再加 7 個 long 型別

我們把上面的Pointer改成下面這個結構:

class Pointer {
    volatile long x;
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long y;
}

再次執行程式,會發現輸出時間神奇的縮短為了695ms

(2)重新建立自己的 long 型別,而不是 java 自帶的 long

修改Pointer如下:

class Pointer {
    MyLong x = new MyLong();
    MyLong y = new MyLong();
}

class MyLong {
    volatile long value;
    long p1, p2, p3, p4, p5, p6, p7;
}

同時把 pointer.x++; 修改為 pointer.x.value++;,把 pointer.y++; 修改為 pointer.y.value++;,再次執行程式發現時間是724ms

(3)使用 @sun.misc.Contended 註解(java8)

修改 MyLong 如下:

@sun.misc.Contended
class MyLong {
    volatile long value;
}

預設使用這個註解是無效的,需要在JVM啟動引數加上-XX:-RestrictContended才會生效,,再次執行程式發現時間是718ms

注意,以上三種方式中的前兩種是通過加欄位的形式實現的,加的欄位又沒有地方使用,可能會被jvm優化掉,所以建議使用第三種方式。

總結

(1)CPU具有多級快取,越接近CPU的快取越小也越快;

(2)CPU快取中的資料是以快取行為單位處理的;

(3)CPU快取行能帶來免費載入資料的好處,所以處理陣列效能非常高;

(4)CPU快取行也帶來了弊端,多執行緒處理不相干的變數時會相互影響,也就是偽共享;

(5)避免偽共享的主要思路就是讓不相干的變數不要出現在同一個快取行中;

(6)一是每兩個變數之間加七個 long 型別;

(7)二是建立自己的 long 型別,而不是用原生的;

(8)三是使用 java8 提供的註解;

彩蛋

java中有哪些類避免了偽共享的干擾呢?

還記得我們前面介紹過的 ConcurrentHashMap 的原始碼解析嗎?

裡面的 size() 方法使用的是分段的思想來構造的,每個段使用的類是 CounterCell,它的類上就有 @sun.misc.Contended 註解。

不知道的可以關注我的公眾號“彤哥讀原始碼”檢視歷史訊息找到這篇文章看看。

除了這個類,java中還有個 LongAdder 也使用了這個註解避免偽共享,下一章我們將一起學習 LongAdder 的原始碼分析,敬請期待。

你還知道哪些避免偽共享的應用呢?


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

雜談 什麽是共享false sharing

完全 分享圖片 throws 其它 也有 ava 循環 哪些 訪問 問題 (1)什麽是 CPU 緩存行? (2)什麽是內存屏障? (3)什麽是偽共享? (4)如何避免偽共享? CPU緩存架構 CPU 是計算機的心臟,所有運算和程序最終都要由它來執行。 主內存(RAM)是數據

雜談 什麼是共享false sharing

問題 (1)什麼是 CPU 快取行? (2)什麼是記憶體屏障? (3)什麼是偽共享? (4)如何避免偽共享? CPU快取架構 CPU 是計算機的心臟,所有運算和程式最終都要由它來執行。 主記憶體(RAM)是資料存放的地方,CPU 和主記憶體之間有好幾級快取,因為即使直接訪問主記憶體也是非常慢的。 如果對一塊資

共享False Sharing

目錄 一、計算機的基本結構 二、快取行 三、偽共享 四、如何避免偽共享 快取系統中是以快取行(cache line)為單位儲存的,當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。 一、計算機的基本結構 下圖是計算的

百度開源分散式id生成器uid-generator原始碼剖析 共享false sharing,併發程式設計無聲的效能殺手 一個Java物件到底佔用多大記憶體? 寫Java也得了解CPU--共享

百度uid-generator原始碼 https://github.com/baidu/uid-generator   snowflake演算法 uid-generator是基於Twitter開源的snowflake演算法實現的。 snowflake將long的64位分為了3部分,時間戳、

百度uid-generator原始碼 共享false sharing,併發程式設計無聲的效能殺手 一個Java物件到底佔用多大記憶體? 寫Java也得了解CPU--共享

https://github.com/baidu/uid-generator   snowflake演算法 uid-generator是基於Twitter開源的snowflake演算法實現的。 snowflake將long的64位分為了3部分,時間戳、工作機器id和序列號,位數分配如下。

共享FalseSharing

快取系統中是以快取行(cache line)為單位儲存的。快取行是2的整數冪個連續位元組,一般為32-256個位元組。最常見的快取行大小是64個位元組。當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能,這就是偽共享。快取行上的寫競爭是執行

Hbase分散式成功實施

Hbase偽分散式(成功實施)hadoop叢集正常,zookeeper叢集正常 修改Hbase-env.sh的JAVA_HOME,改成絕對路徑。 並修改export HBASE_MANAGES_ZK=false 修改環境變數HBASE_HOME 修改hbase-site.xml 新增  

Linux 程序通訊之:記憶體共享Shared Memory

一、簡介 共享記憶體允許兩個程序訪問同一塊記憶體區域,它們使用同一個 key 值標記。 二、特點 優點: 通訊方便,兩個程序也是直接訪問同一塊記憶體區域,減少了資料複製的操作,速度上也有明顯優勢。 缺點: 沒有提供同步機制,往往需要我們使用其它(例如訊號)等手段實

樹莓派/PC實現實時攝像頭資料共享Python—picamera

上次實驗使用Python—OpenCV實現,發現傳輸效果並不是很理想,接下來使用Python和picamera實現樹莓派/PC實時攝像頭資料共享,主要也可分為伺服器和客戶端兩部分。 伺服器Demo如下: import numpy as np import cv2 import socke

樹莓派/PC實現實時攝像頭資料共享Python—OpenCV

使用Python和OpenCV實現樹莓派/PC實時攝像頭資料共享,主要分為伺服器和客戶端兩部分。 伺服器Demo如下: #伺服器端 import socket import threading import struct import time import cv2 import nu

nginx靜態超級簡單

  由於只是學習偽靜態就自己配個host隨便玩了,也沒多寫配置檔案,注意是偽靜態 上面的一條偽靜態(rewrite)是將訪問wojiuwangla.com/wangla.html給301(重定向)到百度。 首先配置host,我的電腦是win10的,host檔案在C:\

Content Provider 之 最終彈 實戰體驗跨程式資料共享結合SQLiteDemo

本模組共有四篇文章,參考郭神的《第一行程式碼》,對Content Provider的學習做一個詳細的筆記,大家可以一起交流一下: 簡單起見,我們還是在之前的DatabaseTest專案(點選前往碼雲地址)的基礎上繼續開發。 需求是:通過內容提供器來給它加人外部訪問介面。 程式設計的

C++之函式物件/函式Function Object詳解

       除了自定義的函式物件,標準庫還為我們提供了一系列現成的函式物件, 比如常見的數學、邏輯運算等。例如:negate<type>(),plus<type>(),minus<type>(),multiplies<type&g

“知識共享CC協議簡單介紹

“知識共享”是 Creative Commons 在中國大陸地區的通用譯名,一般簡稱為 CC。CC 既是該國際組織的名稱縮寫,也是一種版權授權協議的統稱。類似BSD 和 GNU 許可證更廣泛應用於軟體及其原始碼方面,而CC系列協議主要用於文字或藝術類的創

Amazon EC2不同賬號之間例項共享國外服務

1從已有的機器自己製作EC2的AMI   我一直用Amazon AWS提供的免費虛擬主機來服務我的網站可靠雲服務. 根據我的觀察,雖然是t1.micro的機型,但是穩定性非常好,比hostmonster的VPS穩定多了.   然後我用另外一張信用卡申請了另一臺機器,打算再部

java結合wabacus實現session跨域、session共享第二版

宣告:該版相對於第一版有所改進。原因:每一版存在session覆蓋問題,分析如下: request().getServletContext().setAttribute("globelSession", session); 我們可以把globelSession想象成是索引

IIS7下靜態URL重寫的實現方法

2、在web.config裡的<configuration />新增規則,例如:     <system.webServer>       <rewrite>         <rules>           <ru

PHP開啟靜態AppServ伺服器

mod_rewrite是Apache的一個非常強大的功能,它可以實現偽靜態頁面。下面我詳細說說它的使用方法 1.檢測Apache

從Java視角理解系統結構共享

從Java視角理解系統結構連載, 關注我的微博(連結)瞭解最新動態 從我的前一篇博文中, 我們知道了CPU快取及快取行的概念, 同時用一個例子說明了編寫單執行緒Java程式碼時應該注意的問題. 下面我們討論更為複雜, 而且更符合現實情況的多核程式設計時將會碰到的問題. 這些問題更容易犯, 連j

共享(False Sharing)

作者:Martin Thompson  譯者:丁一 快取系統中是以快取行(cache line)為單位儲存的。快取行是2的整數冪個連續位元組,一般為32-256個位元組。最常見的快取行大小是64個位元組。當多執行緒修改互相獨立的變數時,如果這些變數共享同一個快取行,就會無意中影響彼此的效能