Re: [心得] X86 架構下的 Memory Model

作者: sarafciel (Cattuz)   2021-07-20 01:32:17
※ 引述《Instance (呆呆華)》之銘言:
: 大多時間在家有點無聊,花了點時間研究以前一知半解的東西。
: 不過要強調的是,這篇文章只針對 X86。
: std::atomic 有六種 Memory Order 選項:
: memory_order_relaxed,
: memory_order_consume,
: memory_order_acquire,
: memory_order_release,
: memory_order_acq_rel,
: memory_order_seq_cst
: 這六種模式在 X86 底下幾乎沒什麼差別的,
: 用最弱的 memory_order_relaxed 就可以了,
: 因為 X86 是屬於 Strong Memory Model 的架構。
: Load-Load, Store-Store, Load-Store 情況下是安全的。
純看文章敘述而不去找reference是一件有點危險的事情
關於x86_64架構不管是amd還是intel都有留蠻大量的說明文件和手冊在網路上:
Intel® 64 Architecture Memory Ordering White Paper
https://www.cs.cmu.edu/~410-f10/doc/Intel_Reordering_318147.pdf
AMD64 Architecture Programmer’s Manual Volume 2:(請看7.2章)
https://www.amd.com/system/files/TechDocs/24593.pdf
: Store-Load 情況下表示,
: A 執行緒儲存某一變數,
: 其他執行緒必須同步讀到最新的數值,
: 這時就必須用到原子操作。
從這邊開始就錯了
原子操作的目的是為了解決指令的執行粒度過小的問題
以簡單的i++來說,一般在組語層級會變成read-modify-write三道指令執行
當多個core要同時執行i++的時候,有可能core A還在modify階段
core B就做read,但此時i的值還沒有存入core A的計算結果
導致最後core A跟core B在write的時候,有一方的運算是沒有反映在i的值上面
要解決這個問題,就得確保core A或core B在讀取前,
有一方的操作已經完整反映在i上面
所以read-modify-write必須變成一組不可分割的操作
這就是原子操作的由來跟目的,當有某個core在看某個被分享的共同變數x時
要不其它core對這個x什麼都還沒做,要嘛其它core已經把它的計算結果存入x了
所以原PO這邊你在談的東西,並不是atomic
它的專有名詞叫做memory barrier,
在解決的主要是亂序執行跟相依性不可見衍生的問題
因為x86_64設計特性的緣故,atomic本身自帶memory barrier的作用
但這不代表atomic就是memory barrier
就好像折凳可以當武器用,但終究折凳本來誕生的目的是給人坐的
: 如果要理解原子操作的話,
: 最簡單的方法是從硬體角度來思考。
: 現代的 CPU 有 L1, L2, L3 Cache,
: 如果你的電腦有多個核心,
: 當資料放在 L1, L2 Cache 時,
: 並不保證所有核心對某一變數的值是一致的。
well,這裡開始的東西就很複雜了XD
這一段敘述其實是對的,但也不對
L1 L2 cache確實因為是core各自獨有的
連帶會產生存在cache裡的值有同步性問題要解決
但cpu的designer其實並沒有擺爛把這個問題丟給compiler跟programmer去煩惱
原因也很簡單,如果某個共同變數x
會因為compiler跟programmer的不注意就讓core A跟B裡面的值不一樣
那這個共同變數在硬體提供的抽象上是根本失敗的
所以cache跟cache之間,其實是有一個機制在保護共同變數這個抽象的
它的名字叫做cache coherence protocol (快取一致性協議)
上面的AMD64 Architecture Programmer’s Manual Vol.2在7.3章就是這個部份了
它也告訴你amd用的是MOESI這個cache coherence protocol
但是當代的CPU為了得到更好的效能,在CCP這個部分有做一些投機的設計加速
進而導致了快取一致性在某些特定的場合下,
core A對好幾個共同變數的更改順序在core B的視野裡會跟core A不同
這個部分要講細節就必須非常的細節,所以我直接丟reference:
Memory Barriers: a Hardware View for Software Hackers
http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
務必精讀,我找不到比這篇講得更細膩的paper了(逃)
回到memory barrier這件事上,我直接拿裡面的舉例來講:
1 void foo(void)
2 {
3 a = 1;
4 b = 1;
5 }
6
7 void bar(void)
8 {
9 while (b == 0) continue;
10 assert(a == 1);
11 }
文章內在講的是因為store buffer的影響跟共同變數在MESI裡的state不同,
導致執行bar的core看到的a跟b賦值順序有可能是反過來的
但這段code最本質的問題其實是相依性不可見。
foo裡面a跟b的賦值在純粹的context上面你是觀察不出有什麼相依性的
但是在bar裡面a跟b卻有了時序上的相關,
a的assert過不過會被b維持在0這個state有多久某種程度的決定
而相依性不可見這件事,
會直接導致亂序執行(Out of Order Execution,以下簡稱OOE)的假設失靈
CPU因為pipeline設計的緣故,兩條沒有相依性的指令儘管執行結果跟順序無關
但有些場合透過設計指令的執行順序可以減少pipeline的stall
所以執行速度是跟執行順序有關的
(如果看不懂這邊在講什麼,請去找計組跟計結的課本來翻,
pipeline是要在大學花好幾節課講的東西,我不可能在這邊講完它的內容orz)
這也導致了在不違背相依性的前提下,CPU跟compiler都會試著去做一定程度的OOE
所以上述的這段程式碼不是只有在store buffer的影響下會出錯
CPU跟compiler的OOE都有可能導致a=1和b=1的順序調換,
因為這兩行code在foo的視野裡面,你根本看不到有任何相依性,他們看起來是可以換的
所以你必須提供一個機制,讓唯一有機會觀察到相依性的角色-也就是programmer
手動的去告訴CPU跟compiler,在某個時間點你不可以做OOE
不但不能做OOE,還要把之前的讀寫結果明確的在每個core上同步
這個機制就叫memory barrier
要打個比方的話,就像你一直覺得坐你隔壁的同事是正直有為的好青年
直到他確診之前你都不知道他愛去萬華阿公店,這個就是相依性不可見
所以那個有機會觀察到相依性的人,也就是指揮中心做了PCR檢測以後
因為讓你們在外面繼續人與人的連結(OOE)很危險
所以會把跟你同事接觸過的人(包括你)關14天(設memory barrier)
而本質上你有沒有染疫這件事情當下是一個不確定的狀態
於是大家一起關14天,等到所有人的狀態都確定以後再做下一步的決策
: 而進行原子操作的動作之後,
: 變數的值會同步到所有核心的 Cache。
: 原子操作的方法有很多種:
: 1. std::atomic<int> x;
: 2. std::atomic_thread_fence(std::memory_order_relaxed);
: 3. asm volatile("mfence" ::: "memory"); // 組合語言
: 4. asm volatile("lock; addl $0,0(%%rsp)" ::: "memory", "cc"); // 好像是更快的組合
: 語言,我不是很了解
again,x86_64指令集的文獻跟說明其實很多
其中之一就是intel那個4000多頁的說明手冊:
https://tinyurl.com/6dsna7db
LOCK—Assert LOCK# Signal Prefix 在3-592 Vol. 2A
主要是說明LOCK是一個前綴,用來描述下一條指令必須要有atomic的性質
MFENCE—Memory Fence 在4-22 Vol. 2B
這個就是組語層級的total order的memory barrier了
mfence主要是針對cpu的OOE,
compiler的OOE則是由括號裡面的那個"memory"來關掉
Vol. 3A的8.1章開始就是講atomic跟memory ordering相關的部分,
其中的8.2.2節有提到:
‧ Locked instructions have a total order.
所以lock是帶有memory barrier效果的
而且lock還很神奇的比原生的mfence來的快
所以linux kernel裡就直接拿lock接一條廢指令來當作它的mfence
: 5. InterlockedExchange(); // Win API
InterlockExchange就真的是atomic operation了
而不是memory barrier
: 效果都是將變數的值同步到所有核心,
: 這樣才能保證多執行緒環境下此變數的全局可見,
: Win API 或許效能會稍差一點吧。
: 參考文章:
: C++11中的內存模型上篇 - 內存模型基礎
: https://tinyurl.com/f36rsus9
: C++11中的內存模型下篇 - C++11支持的幾種內存模型
: https://tinyurl.com/95e33cf5
: X86/GCC memory fence的一些見解
: https://zhuanlan.zhihu.com/p/41872203
作者: lovejomi (JOMI)   2021-07-20 03:12:00
請問 什麼情況會有memory barrier產生,比方說我有個共用變數a=1後 開一條thread 去讀a,我能保證這條thread一定讀到1嗎?隱約記得之前看資料提到 開thread當下會有memory barrier所以這能被保證...如果這正確,除了開thread,什麼情況也會呢? 常看到有人寫 thread1寫值後 用某系統api(例如win32 autoresetevent)去notify另一條thread2讀值,但他沒有用condition variable or mutex等方法做同步,我再想這種方法是不是很有可能出問題, 以上 謝謝
作者: Instance (呆呆華)   2021-07-20 14:36:00
謝謝分享,再仔細研究看看
作者: eopXD (eopXD)   2021-07-20 16:59:00
推分享~
作者: hare1039 (hare1039)   2021-07-21 07:24:00
作者: F04E (Fujitsu)   2021-07-21 12:10:00
m
作者: darth ( )   2021-07-21 12:43:00
作者: shibin (喜餅)   2021-07-21 12:48:00
作者: milkdragon (謝謝大家!!)   2021-07-21 13:37:00
作者: terter (terter)   2021-07-21 16:47:00
作者: lovejomi (JOMI)   2021-07-21 23:16:00
但我想知道怎麼樣才會用code寫出來memory barrier? 例如我說的 我寫值後開thread去讀值 中間我自以為沒有手動安插任何memory barrier 我能保證另一條thread能讀到最新的數值嗎?如果可以 是為什麼呢?我一直以為不能保證除非你自己補mutex 之類的. 謝謝
作者: james732 (好人超)   2021-07-22 16:09:00
感謝原PO這篇以及下面的補充說明
作者: ioiolo (嘻 =)   2021-08-05 14:01:00
學習了
作者: Ebergies (火神)   2021-08-05 14:59:00
這篇講得很清楚另外如果想要用 C++ atomic 做某些事的話,常常會是直接用 mutex 更好。
作者: kipi91718 (正港台灣人)   2021-08-07 23:09:00
推一個 沒有從assembly code和pipeline的層級想會很難
作者: lc85301 (pomelocandy)   2021-08-09 09:02:00
lovejomi 的問題應該在 OS 的層級就必須對付了我的記憶 FreeRTOS 上就會出現 memory barrier
作者: lolmap (休伊yo)   2021-08-10 00:16:00
推分享
作者: TMDTMD2487 (ㄚ冰)   2021-08-10 19:50:00
lovejomi 你如果是問userspace那熟讀你所使用的threadlibrary提供的manual會明確的講時序相關的保證底層運作如這篇講述的不是一時能講完的,不過Linux有提供文件,參考/Documentation/memory-barriers.txt
作者: x246libra (楓)   2021-08-17 21:32:00
作者: linlin110 (酥炸雞丁佐羅勒)   2021-08-20 00:55:00
版主可以m這篇嗎

Links booklink

Contact Us: admin [ a t ] ucptt.com