惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
小众软件
小众软件
有赞技术团队
有赞技术团队
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
AWS News Blog
AWS News Blog
C
Cisco Blogs
美团技术团队
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
W
WeLiveSecurity
D
DataBreaches.Net
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
T
The Blog of Author Tim Ferriss
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Vercel News
Vercel News
月光博客
月光博客
T
Tailwind CSS Blog
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
V2EX
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
并发编程系列(二)—— store, load 与 RMW
同勉共进 · 2026-04-27 · via 博客园_首页

背景知识

store buffer 与 L1/L2/L3 cache

注意,store buffer 不是缓存,它属于 CPU 而不是缓存系统。

不同 CPU 都有 store buffer 吗?

现代高性能的超标量处理器几乎都配备了 Store Buffer,可能叫法不同,但都有类似的“东西”和机制。

什么是“写完成”?

store操作完成的标志是什么?如果只是写到 store buffer,那么在架构层面上(Architecturally)对全局依然是“未发生”的,因为store完成的标志是“写入缓存,让别人看见”。

Store-Load 重排

先看一个例子:

初始值:
x = 0, y = 0
thread 1:               thread2:
x.store(1)    -- A       y.store(1)    -- C
a = y.load()  -- B       b = x.load()  -- D

时序:

1. thread 1 对 x 执行写操作,但只是把 1 写入 store buffer
2. 同时,thread 2 对 y 执行写操作,但也只是把 1 写入 store buffer
3. thread 1 在没有把 store buffer 刷入 L1 cache 的前提下,就率先读了 y 的值
  (Store-Load 重排),而此时,thread 2 对 y 的写也还停留在 store buffer,
   对 thread 1 不可见,于是,thread 1 读到的 y 值是 0。
4. 同理,thread 2 读到的 x 值也是 0。
5. thread 1 和 thread 2 把各自的store buffer 刷到了 L1 cache,但为时已晚,因为 load
   先一步执行了,a == 0 && b == 0 的事实已经铸就。

基于代码顺序,有 A --> B 并且 C --> D。

从 thread 1 的视角看,既然它读到了y的旧值,那么有 B--> C。

从 thread 2 的视角看,既然它读到了x的旧值,那么有 D --> A。

从 thread 1 的视角看,总体顺序是 A --> B --> C --> D

从 thread 2 的视角看,总体顺序是 C --> D --> A--> B

即,不同线程看到的全局顺序不一样,并且,两者结合起来,形成了 A--> B --> C -->D --> A 这样的环。

RMW (atomic read-modify-write operation) 和 load 的区别

  1. 从“自我”的视角看,RMW / load 都是从 cache 或 main memory 中读取,至少在读到的那一刻,它们看到的都是最新的值。
  2. 如果在读的一瞬间,其它线程修改了同一个原子变量,但只写到了 store buffer,那么,从架构层面上看,这个“写”并没有完成,所以不算“最新值”。因此,这种情况不影响第1条中“两者都读到最新值”的结论。
  3. 两者的区别在于“对他人的影响”上。
    1. RMW 会先通过 RFO (Read For Ownership) 请求要求其它核心交出最新值并作废自己 cache 中的副本。
    2. 在“读”动作完成后,RMW 会锁住对应的 cache line 直到整个 RMW 操作完成,这期间,别的线程既不能读,更不能写这条 cache line。
    3. 普通 load 只是自己读,不干涉别人,它读的时候别人可能也在读,在它刚读完的一瞬间,别人就可能修改了这个值。

RMW 和 store 的区别

store可能会将数据放入 store buffer,导致延迟可见,但 RMW 一定不会。

  • 当 CPU store某个变量时,如果该变量所在的 Cache Line 不在 L1 Cache 中,或者状态不是 Exclusive / Modified(味着其他核心也缓存了该数据,需要通过总线发送 Invalidate 消息让其他核心作废缓存),CPU 必须向总线发送信号获取该缓存行的所有权。等待缓存行返回需要几百个时钟周期,为了提效,CPU 会把要写入的值暂存进 Store Buffer,然后继续执行下一条指令。此时,这个写操作对其他 CPU 是不可见的,直到 Store Buffer 最终将数据刷入 L1 缓存。这就是普通 store 导致延迟可见的物理根源。
  • RMW 在读取阶段就已经独占了 cache line,写入的数据会直接放入 cache line。RMW 执行完成,释放 cache line 锁的那一刻,数据就全局可见了。

RMW 会产生全内存屏障 (full memory barrier) 吗?

x86 会,ARM 不会。

x86 下,RMW 会被翻译成lock前缀或者隐式lock前缀(就是不显式携带lock但有lock的功能)的指令,例如,x86-64下,fetch_add被翻译成lock xadd指令。无论是显式 lock(比如 lock xadd)还是隐式 lock,都会生成全内存屏障 (Full Memory Barrier),即,在当前 CPU core上:

  • 在 lock 指令执行之前的所有读写操作,必须在 lock 指令执行完毕前完成;
  • 会强制将当前核心的 store buffer 刷入 L1 Cache,确保 lock 之前的读写操作,对其它核心都可见。
  • 在 lock 指令之后的所有读写操作,必须等 lock 指令完成后才能开始。

ARM 采用弱内存模型,硬件有更大的自由度。比如,data.fetch_add(1, std::memory_order_release)会被翻译成ldxrstlxr的组合(详见下文),只具备 release 语义,不具备 acquire 语义。即只能保证 fetch_add前面的指令不会被重排到fetch_add后面,但不能保证fetch_add后面的指令一定不会跑到fetch_add的前面。

full memory barrier 的理解

双向的。即

=========== 前向屏障:前面的指令不能越过 ===========
会产生 full memory barrier 的指令,比如 lock xadd
=========== 后向屏障:后面的指令不能越过 ===========

即:
1. lock xadd 前的指令,不能排到 lock xadd 后面,换言之,在 lock xadd 被 CPU 执行之前,它前面
   的指令,都必须被CPU执行完,且对全局可见。
2. lock xadd 后的指令,不能排到 lock xadd 前面,换言之,在 lock xadd 被 CPU 执行之前,它后面
   的指令,都不能先于它被 CPU 执行。

store/load/ RMW 对应的汇编指令

原子变量以std::atomic<long long> data{0}为例,RMW 操作以fetch_add为例,编译器为 GCC 15.2,通过 Compiler Explorer 来观察生成的汇编代码。

指令 内存序 X86_64 ARM64 ARM64 (-mno-outline-atomics)[1]
store[2] relaxed movq str str
release stlr stlr
seq_cst xchgq
load[3] relaxed movq ldr ldr
acquire ldar ldar
seq_cst
fetch_add relaxed lock xadd __aarch64_ldadd8_relax ldxr & stxr
release __aarch64_ldadd8_rel ldxr & stlxr
acquire __aarch64_ldadd8_acq ldaxr & stxr
acq_rel __aarch64_ldadd8_acq_rel ldaxr & stlxr
seq_cst

备注:

  1. ARM 架构下默认编译成内置函数,加上-mno-outline-atomics参数后,编译成汇编指令。
  2. store只支持这3中内存序,若使用其它内存序,则是未定义行为。
  3. load只支持这3中内存序,若使用其它内存序,则是未定义行为。

x86_64 体系下的store/load/ fetch_add

store(relaxed)store(release)

为什么两者都翻译成movq

mov 指令天然的原子性

x86_64 采用 TSO (Total Store Order) 这种强内存模型,对自然对齐的(32位、64位)内存进行读写,mov 指令(movlmovq等)在硬件层面就保证了原子性。

架构天然的 release 语义

什么是 release 语义?

当前线程内,store操作前的任何读写,包括非原子变量的读写,都不能被重排到该store操作的后面。

为什么天然?

背景:先要搞懂什么是 CPU 的乱序执行(指令重排)。汇编代码中,是指令 A 在前,指令 B 在后,而 CPU 在实际执行是,可能先执行指令 B,后执行指令A,这就是所谓的 CPU 乱序执行。

如前所述,x86_64 采用 TSO 强内存模型,在硬件层面就禁止了下面这两种重排:

  • 禁止 Load-Store 重排:如果汇编代码是 读操作 A --> 写操作 B,那么,CPU在执行时,绝不可能先执行 B 后执行 A。
  • 禁止 Store-Store 重排:如果汇编代码是 写操作 A --> 写操作 B,那么,CPU在执行时,绝不可能先执行 B 后执行 A。

综上,写操作前面的读写操作,都不能跑到该写操作的后面,天然满足 release 语义。

特别注意:release 语义是由 x86 体系结构(硬件)保证的,不是由 mov 指令保证的!mov 指令只能保证原子性,但该指令本身不禁止任何重排。

两者等价吗?

不等价

如上,两者翻译成相同的汇编指令,只能说是在 CPU 层面(硬件层面)等价。但在编译器层面(软件层面)不会等价。

从源代码到程序执行(进程/线程),可以简化成下面的模型(只为说明问题,所以只列出关键节点,不要扣细节上的对错):

源代码 --- 编译器进行汇编 ---> 汇编指令 --- CPU 执行 ---> 程序跑起来(进程 / 线程)

前面说过,CPU执行指令时,可能不会严格按照开发者看到的汇编代码的顺序执行。类似的,从源代码到汇编指令这一步,编译器也可能会重排。比如:

int data = 0;
std::atomic<int> flag{0};

// 情况 A:使用 release
void producer_release() {
    data = 42;                                // (1) 普通写
    flag.store(1, std::memory_order_release); // (2) 原子写
}

// 情况 B:使用 relaxed
void producer_relaxed() {
    data = 42;                                // (3) 普通写
    flag.store(1, std::memory_order_relaxed); // (4) 原子写
}

对于 producer_release():
编译器知道 (2) 是 release 语义,它必须保证data = 42flag = 1之前发生。生成的 x86 汇编大概是这样:

mov DWORD PTR data[rip], 42  ; 先写 data
mov DWORD PTR flag[rip], 1   ; 后写 flag (release)

对于 producer_relaxed():
因为是 relaxed,编译器发现dataflag是没有数据依赖的两个变量,为了优化寄存器分配或者流水线效率,编译器完全有可能把代码重排成这样

mov DWORD PTR flag[rip], 1   ; 编译器擅自把写 flag 提前了!(relaxed 允许这么做)
mov DWORD PTR data[rip], 42  ; 后写 data

所以,对开发者而言,如果想要 release 语义,就老老实实指定std::memory_order_release

load(relaxed)load(acquire)

store的情况类似,两者翻译成movq的原因是 TSO 内存模型也禁止 Load-Load 重排。因此:

TSO 内存模型禁止 Load-Load 和 Load-Store 重排 -->
    读指令后面的任何读写指令,都不可能排到该读指令前面 -->
        天然满足 acquire 语义

同样地,两者只是在 CPU 层面等价,但因为编译器重排的存在,两者整体上不等价。

store(seq_cst)load(seq_cst)

为什么两者翻译成的汇编代码不一样?

先说语义:

  • seq_cst作用在store上,具有 release 语义。(store本身就不可能有 acquire语义)
  • seq_cst作用在load上,具有 acquire 语义。
  • 除此之外,seq_cst还保证:程序中所有标记为**seq_cst**的原子操作都有一个全局唯一的执行顺序,并且所有线程看到的这个顺序都是相同的,即所谓的全局单一全序 (Single Total Order)。注意,这个约束只针对所有使用了seq_cst内存序的原子操作,而不是任意的原子操作。

前两点,x86 上已经在硬件层面天然保证了,关键看第三点。

A:  data_x.store(1, seq_cat)
  --> 假设翻译成 movq %edi, data_x(%rip)
    --> x86体系结构保证了,这个 movq 前面的读写指令都必须先于它被CPU执行(禁止 Load-Store / Store-Store重排)
        x86体系结构保证了,这个 movq 后面的写指令绝不能先于它被CPU执行(禁止 Store-Store 重排)
B:  data_y.load(seq_cst)
  --> 假设翻译成 movq data_y(%rip), %rax
    --> x86体系结构保证了,这个 movq 后面的读写指令绝不能先于它被CPU执行(禁止 Load-Load/ Load-Store重排)
        x86体系结构保证了,这个 movq 前面的读指令必须先于它被CPU执行(禁止 Load-Load 重排)

可见,光凭架构本身的约束,是不能禁止 B 跑到 A 前面(即 B 先于 A 被 CPU 执行 )的,因为 x86 本身不禁止 Store-Load 重排,这样,就违背第三点约束了。(Store-Load 重排的本质原因是 store buffer 的存在,详见前文对 Store-Load 重排的介绍)。

要保证第3点,除了强制 CPU 刷新 store buffer,避免 Store-Load 重排之外,还需要保证“不同原子操作的结果,被所有线程以相同的顺序看到”,比如原子操作 A 和 B,所有线程要么都先看到 A 的结果,后看到 B 的结果,要么反过来。不能有的线程先看到 A 的结果,有的线程先看到 B 的结果。比如典型的非多副本原子性 (Non-Multi-Copy Atomicity) 问题

非多副本原子性 (Non-Multi-Copy Atomicity) 问题

(即使全是读操作且不重排,不同观察者看到的世界也会不同)

这其实就是 IRIW(Independent Reads, Independent Writes)现象的根源。我们明确规定没有任何线程发生指令重排

初始:x = 0, y = 0

线程 1 (Core 1) 线程 2 (Core 2) 线程 3 (Core 3, 观察者) 线程 4 (Core 4, 观察者)
x = 1; y = 1; while(x == 0); while(y == 0);
r1 = y; r2 = x;

假设硬件拓扑结构是:Core 1 和 Core 3 物理距离近(比如共享 L2 缓存),Core 2 和 Core 4 物理距离近。

严格按序执行的过程:

  1. 线程 1 写入 x=1。这个消息很快传到了离它近的 Core 3,但还需要一段时间才能传到 Core 4。
  2. 线程 2 写入 y=1。这个消息很快传到了离它近的 Core 4,但还需要一段时间才能传到 Core 3。
  3. Core 3(线程 3)严格按序执行:它先看到了 x=1,跳出循环执行 ②。此时 y=1 的消息还没传到 Core 3,所以它读到 r1 = y = 0
  4. Core 4(线程 4)严格按序执行:它先看到了 y=1,跳出循环执行 ④。此时 x=1 的消息还没传到 Core 4,所以它读到 r2 = x = 0

结果: 同样出现了 r1 == 0r2 == 0

分析:
注意!线程 3 绝对没有把 ① 和 ② 重排,线程 4 也绝对没有重排 ③ 和 ④。
它们看到的分歧,纯粹是因为在多核系统中,数据在总线/缓存网络中的传播速度不一样(这在 ARM 和 PowerPC 等架构中是真实存在的)。

  • 线程 3 认为:x=1 发生在了 y=1 之前。
  • 线程 4 认为:y=1 发生在了 x=1 之前。
    它们又一次对“全局顺序”产生了分歧。

解决:

想要解决这个问题,系统必须保证,一个写操作要么对所有核心都不可见,一旦对某个核心可见,就必须瞬间对所有核心可见。不能出现“离得近的先看到,离得远的后看到”的情况。具体的,就是需要强迫缓存一致性协议(比如 MESI 等)进行昂贵的同步。

要解决上面的问题,就需要加 full memory barrier。一个显而易见的方法是在 A 和 B 上都加一个 full memory barrier,但这是冗余的,影响效率,因为只在某一个上面加就行了(详见前面对 full memory barrier 的解读)。

那么,加在谁上面呢?经过统计,多数程序中,都是读操作多于写操作,因此,加到写操作上,对效率的影响更小。所以,store(seq_cst)被翻译成了带隐式lock的 exchange 指令(xchglxchgq等),而load(seq_cst)依然被翻译成 mov 指令。

fetch_add

在 x86 上,fetch_add被统一翻译成lock xadd,即采用了最严格的内存序,加了 full memory barrier,为的就是保证“读-改-写”这一套流程的整体原子性,以及“结果对所有线程立即可见”(详见背景知识部分对 RMW 的介绍)。

所以,如果光从 CPU 的角度看,采用哪种内存序都是一样的。但是,如前所述,在编译器层面,不同的内存序依然是不一样的。

ARM64 体系下的store/load/ fetch_add

storeload

store(relaxed)load(relaxed)

两者不要求任何同步或者顺序约束,只要求原子性,因此翻译成普通的str((Store Register))和ldr((Load Register))指令即可。

store(release)/store(seq_cst)/load(acquire)/load(seq_cst)

为什么store(release)/store(seq_cst)都翻译成stlr(Store-Release Register),load(acquire)/load(seq_cst)都翻译成ldar(Load-Acquire Register)呢?

显然,两者分别满足 Release 语义和 Acquire 语义,所以用于store(release)load(acquire)是没问题的,那为什么可以用于store(seq_cst)load(seq_cst)呢?seq_cst要求的“全局单一全序 (Single Total Order)”如何来保证呢?

原因在于,ARMv8 的硬件内存模型设计得非常强,**stlr****ldar**指令的组合,不仅能满足 Acquire-Release 语义,在硬件层面本身就直接提供了 Sequential Consistency (SeqCst) 的保证

  • 硬件层面保证了:如果代码中先执行了一个stlr指令,随后又执行了一个ldar指令,CPU 硬件绝对不会把它们重排(No Store-Load Reordering)。详见官网文档
  • 另外,ARMv8 内存模型具备多副本原子性 (Multi-copy atomicity),即当一个stlr写入的值被某一个观察者(线程)看到时,它必然同时被所有观察者看到。详见官网文档

综合以上两点,ARM 硬件在执行全是stlr和ldar的代码时,天然就形成了一个所有线程一致认同的 Single Total Order。换言之,此处 ARM 硬件提供了比C++ 标准更强的保证。

xchgq: exchange,q 表示 Quadword,即64位。

该命令自带隐式 lock 语义。当 xchg 的操作数之一是内存地址时,硬件会自动为它加上 lock 语义。注意,如果 xchg 只在两个寄存器之间进行,则没有 lock 语义,只是普通的寄存器操作。

fetch_add

原子性的实现

data.fetch_add(1, std::memory_order_relaxed)为例,实际生成的汇编指令是下面这样:

.L6:
    ldxr    x2, [x1]
    add     x3, x2, 1
    stxr    w4, x3, [x1]
    cbnz    w4, .L6

ARM 架构没有类似 x86 的lock xadd单条原子指令,而是使用 独占加载/存储(Load-Exclusive / Store-Exclusive,即 LL/SC 机制) 配合循环来实现"读-改-写"的原子操作。

  • ldxr x2, [x1]: Load Exclusive Register。从x1指向的地址(即data)加载 64 位数据到x2。Exclusive 表示标记该内存地址的“独占监视器”,为后续的写操作做准备。
  • add x3, x2, 1: 计算x2 + 1,结果存入x3(即准备写入的新值)。
  • stxr w4, x3, [x1]: Store Exclusive Register。尝试x3(新值)写入x1指向的地址。写入的结果(成功或失败)会存入w4(32位寄存器):成功写入为 0,失败为 1。如果在此期间其他线程修改了该地址,独占监视器会被清除,导致写入失败。
  • cbnz w4, .L6: Compare and Branch if Not Zero。如果w4不为 0(说明写入失败,发生了并发竞争),则跳回.L6重新执行“加载-修改-写入”的循环。

内存序的实现

不难看出,内存序的实现,是通过替换 load / store 指令来试下的。例如,要实现 release 语义,就将stxr指令换成stlxr(Store Release Exclusive Register);要实现 acquire 语义,就将ldxr指令换成ldxar(Load Acquire Exclusive Register);要实现seq_cst语义,就同时替换两者。

与 x86 下fetch_add的区别

x86 下的fetch_add,所有内存序都与seq_cst等价,是最严格的,会产生 full memory barrier。

而 ARM 下,实现的是对应的内存序,不存在“越级”。