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