C和C++中的volatile、記憶體屏障和CPU快取一致性協議MESI

一見發表於2019-01-27

目錄

1. 前言 2

2. 結論 2

3. volatile應用場景 3

4. 記憶體屏障(Memory Barrier) 4

5. setjmp和longjmp 4

1) 結果1(非優化編譯:g++ -g -o x x.cpp -O0) 5

2) 結果2(優化編譯:g++ -g -o x x.cpp -O2) 6

6. 不同CPU架構的一致性模型 6

7. x86-TSO 7

8. C++標準庫對記憶體順的支援 7

1) 標頭檔案<stdatomic.h> 7

2) 標頭檔案<atomic> 8

附1:CPU、快取和主存 8

第三級快取(L3 Cache)多核共享: 8

附2:SMP對稱多處理器結構 9

附3:線上C++編譯器 9

附4:資源連結 10

1) C++標準委員會(The C++ Standards Committee) 10

2) 標準C++基金會 10

3) C++之父 10

4) Linux核心關於volatile的說明 10

5) Intel記憶體模型(Intel Memory Model) 10

6) Intel TSO記憶體模型 10

7) Sequential Consistency &TSO 10

8) Write buffer 10

9) x86-64和IA-32開發手冊 10

10) 編譯器屏障(Compiler Barriers) 10

11) C ++ 11中memory_order_consume的作用 10

12) MESI(多核CPU快取一致性協議) 10

13) MESIF(多核CPU快取一致性協議) 10

 

 

  1. 前言

本文內容主要針對Linux,而且主要是x86環境。先看一常見用法:

class Thread {

public:

    X()

        : _stop(false) {

    }

    

    void stop() {

        _stop = true;

    }

    

    void run() {

        while (!_stop) {

            work();

        }

    }

    

private:

    volatile bool _stop;

};

 

然後看看標準C++基金會https://isocpp.org)怎麼說的(官方連結):

結論

  1. 與平臺無關的多執行緒程式,volatile幾乎無用(Java和C#中的volatile除外);
  2. volatile不保證原子性(一般需使用CPU提供的LOCK指令);
  3. volatile不保證執行順序;
  4. volatile不提供記憶體屏障(Memory Barrier)和記憶體柵欄(Memory Fence);
  5. 多核環境中記憶體的可見性和CPU執行順序不能通過volatile來保障,而是依賴於CPU記憶體屏障。

注:volatile誕生於單CPU核心時代,為保持相容,一直只是針對編譯器的,對CPU無影響。

 

volatile在C/C++中的作用:

  1. 告訴編譯器不要將定義的變數優化掉;
  2. 告訴編譯器總是從快取取被修飾的變數的值,而不是暫存器取值。

 

就前言中的程式碼,可移植的實現方式為:

#include <atomic>

 

class Thread {

public:

    X()

        : _stop(false) {

    }

    

    void stop() {

        _stop = true;

    }

    

    void run() {

        while (!_stop) {

            work();

        }

    }

    

private:

    std::atomic<bool> _stop;

};

 

不過這要求至少C++11,否則可使用相容C++98的實現CAtomic<bool>替代:

https://github.com/eyjian/libmooon/blob/master/include/mooon/sys/atomic.h,實際上Linux核心原始碼都帶有這些基礎設施。

  1. volatile應用場景
  1. 訊號處理程式;
  2. 與硬體打交道(嵌入式開發用得多);
  3. setjmp、longjmp配合(請參見:http://www.cplusplus.com/reference/csetjmp/setjmp/),原因同訊號處理。
  1. 記憶體屏障(Memory Barrier)

記憶體屏障,也叫記憶體柵欄(Memory Fence)。分編譯器屏障(Compiler Barrier,也叫優化屏障)和CPU記憶體屏障,其中編譯器屏障只對編譯器有效,它們的定義如下表所示(限x86,其它架構並不相同):

#define barrier() \

    __asm__ __volatile__("":::"memory")

編譯器屏障,如果是GCC,則可用__sync_synchronize()替代

#define mb() \

    __asm__ __volatile__("mfence":::"memory")

CPU記憶體屏障,還分讀寫記憶體屏障

#define rmb() \

    __asm__ __volatile__("lfence":::"memory")

CPU讀(Load)記憶體屏障

#define wmb() \

    __asm__ __volatile__("sfence":::"memory")

CPU寫(Store)記憶體屏障

 

x86架構的CPU記憶體屏障程式碼可在核心原始碼的arch/x86/include/asm/barrier.h中找到。對於原子操作,需要使用CPU提供的“lock”指令,對於CPU亂序需使用CPU記憶體屏障。

程式碼順序

編譯器順序

CPU順序

a=1;

b=x;

c=z;

d=2;

a=1;

d=2;

b=x;

c=z;

b=x;

c=z;

a=1;

d=2;

 

推薦資料:

https://mariadb.org/wp-content/uploads/2017/11/2017-11-Memory-barriers.pdf

 

記憶體屏障進一步還分讀記憶體屏障和寫記憶體屏障等,對非核心開發者來說,記憶體屏障的主要應用場景為無鎖程式設計(Lock-free),因為像pthread_mutx_t等實際已經包含了“Lock”和“Memory Barrier”,所以無需再操心。

  1. setjmp和longjmp

在C/C++中,goto關鍵詞只能函式內的區域性跳轉,函式間的跳轉需要使用setjmp和longjmp,這也是有些協程庫基於setjmp和longjmp實現的原因。

  1. setjmp

儲存上下文,包括訊號掩碼,類似於setcontext。該函式的返回值比較特別,第一次返回0,第二次返回的longjmp第二個引數值(如果longjmp第二個引數值為0,則返回值為1,這樣方便區分於第一次返回)。

  1. longjmp

該函式從不返回,而是跳回到setjmp儲存點,類似於swapcontext。如果沒有先呼叫setjmp,則longjmp的行為是未定義的。C++程式碼可能還會執行棧展開(Unwinding),如果呼叫了任何非平凡解構函式(non-trivial destructors,需顯示處理的解構函式,如記憶體釋放),也會導致未定義的行為。

  1. 程式碼示例(摘自http://www.cplusplus.com/reference/csetjmp/setjmp/

/* x.cpp */

/* setjmp example: error handling */

#include <stdio.h>      /* printf, scanf */

#include <stdlib.h>     /* exit */

#include <setjmp.h>     /* jmp_buf, setjmp, longjmp */

 

struct X {

  X() { fprintf(stderr, "X::ctor\n"); }

  ~X() { fprintf(stderr, "X::dtor\n"); }

};

 

int main()

{

  jmp_buf env;

  int val = -1;

  int m = -1;

  volatile int n = -1;

  X x;

 

  val = setjmp(env);

  fprintf(stderr, "setjmp return: %d\n", val);

  if (val) {

    fprintf(stderr, "m: %d\n", m);

    fprintf(stderr, "n: %d\n", n);

    exit(val);

  }

  else {

    m = 2018;

    n = 2018;

 

    /* code here */

    longjmp(env, 19);   /* signaling an error */

    return 0;

  }

}

 

 

上例程式碼執行有兩種輸出結果:

  1. 結果1(非優化編譯:g++ -g -o x x.cpp -O0)

X::ctor

setjmp return: 0

setjmp return: 19

m: 2018

n: 2018

 

非優先編譯時,總是從記憶體取值。

 

  1. 結果2(優化編譯:g++ -g -o x x.cpp -O2)

X::ctor

setjmp return: 0

setjmp return: 19

m: -1

n: 2018

 

因m未加volatile修飾,直接讀取暫存器值,因此結果是-1。從這裡也可以看出,即使是單執行緒程式,volatile也是必要的,也說明volatile並不是完全沒用,只是它不能幫助解決多執行緒的原子性、記憶體屏障和CPU亂序執行。

另外可發現,上列程式碼的類X的析構未執行,但若將exit改成return,則會執行類X的析構,遇到“}”和“return”時,編譯器會安插解構函式呼叫。

  1. 不同CPU架構的一致性模型

注:LOAD操作,STORE操作。

 

Loads reordered after loads

Loads reordered after stores

Stores reordered after stores

Stores reordered after loads

Atomic reordered with loads

Atomic reordered with stores

Dependent loads reordered

Incoherent instruction cache pipeline

Alpha

Y

Y

Y

Y

Y

Y

Y

Y

ARMv7

Y

Y

Y

Y

Y

Y

 

Y

PA-RISC

Y

Y

Y

Y

 

 

 

 

POWER

Y

Y

Y

Y

Y

Y

 

Y

SPARC RMO

Y

Y

Y

Y

Y

Y

 

Y

SPARC PSO

 

 

Y

Y

 

Y

 

Y

SPARC TSO

 

 

Y

 

 

 

 

Y

x86

 

 

Y

 

 

 

 

Y

x86 oostore

Y

Y

Y

Y

 

 

 

Y

AMD64

 

 

 

Y

 

 

 

 

IA-64

Y

Y

Y

Y

Y

Y

 

Y

z/Architecture

 

 

 

Y

 

 

 

 

 

四種SMP架構的CPU記憶體一致性模型:

  1. 順序一致性模型(SC,Sequential Consistency,所有讀取和所有寫入都是有序的);
  2. 寬鬆一致性模型(RC,Relaxed Consistency,允許某些可以重排序),ARM和POWER屬於這類;
  3. 弱一致性模型(WC,Weak Consistency,讀取和寫入任意重新排序,僅受顯式記憶體屏障限制);
  4. 完全儲存排序(TSO,Total Store Ordering),SPARC和x86屬於這種型別,只有“store load”一種情況會發生重排序,其它情況和SC模型一樣。
  1. x86-TSO

x86-TSO是Intel推出的一種CPU記憶體一致性模型,特點是隻有“Store Load”一種情況會重排序,也就是“Load”可以排(亂序)在“Store”的前面,因此不需要“Load Load”、“Store Store”和“Load Store”這三種屏障。

  1. “Store Load”屏障的作用是:確保“前者刷入記憶體”的資料對“後者載入資料”是可見的;
  2. “Load Load”屏障的作用是:確保“前者裝載資料”先於“後者裝載指令”;
  3. “Store Store”屏障的作用是:確保“前者資料”先於“後者資料”刷入記憶體,且“前者刷入記憶體的資料”對“後者是可見的”;
  4. “Load Store”屏障的作用是:確保“前者裝載資料”先於“後者重新整理資料到記憶體”。

  1. C++標準庫對記憶體順的支援
  1. 標頭檔案<stdatomic.h>

enum memory_order {

    memory_order_relaxed, // 寬鬆一致性模型,不對執行順序做任何保證

    memory_order_consume, // (讀操作)本執行緒所有後續有關本操作的必須在本操作完成後執行

    memory_order_acquire, // (讀操作)本執行緒所有後續的讀操作必須在本條操作完成才能執行

    memory_order_release, // (寫操作)本執行緒所有之前的寫操作完成後才執行本操作

    memory_order_acq_rel, // (讀-修改-寫)同時包含Acquire和Release

    memory_order_seq_cst  // (讀-修改-寫,預設型別)順序一致性模型,全部順序執行

};

  1. 標頭檔案<atomic>

// 預設記憶體順型別為“memory_order_seq_cst”

std::atomic<bool>

std::atomic<int32_t>

std::atomic<int64_t>

。。。。。。

附1:CPU、快取和主存

第三級快取(L3 Cache)多核共享:

附2:SMP對稱多處理器結構

多個CPU對稱工作沒有區別,無主次或從屬關係,平等地訪問記憶體、外設和一個作業系統,共享全部資源,如匯流排、記憶體和I/O系統等,因此也被稱為一致儲存器訪問結構(UMA : Uniform Memory Access)。

其它的架構有:

  1. NUMA(Non-Uniform Memory Access,非統一記憶體訪問),基本特徵是將CPU分成多個模型,每個模型多個CPU組成,具有獨立的本地記憶體和I/O槽口等;
  2. MPP(Massive Parallel Processing,海量並行處理結構),基本特徵是由多個SMP伺服器(每個SMP伺服器稱節點)通過節點網際網路絡連線而成,每個節點只訪問自己的本地資源(記憶體、儲存等),是一種完全無共享(Share Nothing)結構。

附3:線上C++編譯器

  1. https://www.tutorialspoint.com/compile_cpp_online.php
  2. https://www.jdoodlecom/online-compiler-c++
  3. http://coliru.stacked-crooked.com/
  4. https://www.onlinegdb.com/online_c++_compiler
  5. https://ideone.com/
  6. http://cpp.sh/
  7. https://www.cppbuzz.com/compiler/online-c++-compiler
  8. https://www.codechef.com/ide/
  9. https://repl.it/repls/ZestyNaturalMatch
  10. https://www.codiva.io
  11. http://codepad.org/
  12. https://cppinsights.io/
  13. https://tio.run/#cpp-gcc
  14. http://www.compileonline.com/

聚合了各種語言線上編譯。

  1. https://rextester.com/l/cpp_online_compiler_gcc

還支援其它眾多語言線上編譯。

附4:資源連結

  1. C++標準委員會(The C++ Standards Committee)

http://www.open-std.org/jtc1/sc22/wg21/

  1. 標準C++基金會

https://isocpp.org

  1. C++之父

http://www.stroustrup.com/

  1. Linux核心關於volatile的說明

https://www.kernel.org/doc/html/latest/process/volatile-considered-harmful.html

  1. Intel記憶體模型(Intel Memory Model)

https://en.wikipedia.org/wiki/Intel_Memory_Model

  1. Intel TSO記憶體模型

https://www.cl.cam.ac.uk/~pes20/weakmemory/x86tso-paper.tphols.pdf(A Better x86 Memory Model: x86-TSO)

http://homepages.inf.ed.ac.uk/vnagaraj/papers/hpca14.pdf(TSO-CC: Consistency directed cache coherence for TSO)

  1. Sequential Consistency &TSO

https://www.cis.upenn.edu/~devietti/classes/cis601-spring2016/sc_tso.pdf

  1. Write buffer

https://en.wikipedia.org/wiki/Write_buffer

  1. x86-64和IA-32開發手冊

https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html(IA-32:Intel Architecture 32-bit,即32位x86)

  1. 編譯器屏障(Compiler Barriers)

https://www.oracle.com/technetwork/server-storage/solarisstudio/documentation/oss-compiler-barriers-176055.pdf

  1. C ++ 11中memory_order_consume的作用

https://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/

  1. MESI(多核CPU快取一致性協議)

https://en.wikipedia.org/wiki/MESI_protocol

  1. MESIF(多核CPU快取一致性協議)

https://en.wikipedia.org/wiki/MESIF_protocol

M

修改(Modified)

該Cache line(快取行)有效,資料被修改(dirty)了,和主存中的資料不一致,資料只存在於本Cache中

E

獨佔互斥(Exclusive)

該Cache line只被快取在該CPU的快取中,它是未被修改過的(clean),與主存中資料一致

S

共享(Shared)

該Cache line有效,資料和記憶體中的資料一致,資料存在於很多Cache中

I

無效(Invalid)

該Cache line無效,可能有其它CPU修改了該Cache line

F

轉發(Forward)

Intel提出來的,意思是一個CPU修改資料後,直接針修改的結果轉發給其它CPU

 

相關文章