|
| 1 | +Title: C++ 的 `volatile`:它到底管啥、不管啥(CppCon 2024 全面笔记) |
| 2 | +Date: 2025-10-01 09:00 |
| 3 | +Tags: C++, volatile, 编译器优化, 并发, 内存模型, CppCon2024 |
| 4 | +Slug: volatile-in-cpp-cppcon-2024-notes |
| 5 | + |
| 6 | +> CppCon 2024 — What Volatile Means (and Doesn’t Mean), Ben Saks |
| 7 | +> ([video][1], [slides][2]) |
| 8 | +
|
| 9 | +<div class="alert alert-warning" role="alert"> |
| 10 | + ⚠️ 本文根据视频字幕和 slides 由 AI 生成 |
| 11 | +</div> |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 0. 背景与提纲 |
| 16 | + |
| 17 | +* `volatile` 的使命:**阻止编译器做会伤害程序语义的某些优化**。很多 C++ 程序员并不清楚它到底提供哪些保护、又**不**提供哪些保护,从而误用:要么不必要地关掉了优化,要么以为得到了保护但其实没有,导致隐蔽的运行时缺陷。 |
| 18 | +* 本次内容覆盖: |
| 19 | + |
| 20 | +1. 为什么需要 `volatile`; |
| 21 | +2. 声明里 `volatile` 的正确放置方法; |
| 22 | +3) 它能/不能提供的保护; |
| 23 | +4) 编译器把 `volatile` 搞砸时的几种work around |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## 1. 为什么需要 `volatile`:从 UART 驱动说起 |
| 28 | + |
| 29 | +### 1.1 设备寄存器与内存映射 |
| 30 | + |
| 31 | +* 设备驱动通过**设备寄存器**与硬件通信;寄存器可能用于:控制(control)、状态(status)、发送(transmit)、接收(receive)、波特率等(以 ARM E7T UART 为例)。 |
| 32 | +* E7T 的两个 UART 具有相同寄存器布局(偏移与功能): |
| 33 | + |
| 34 | + ``` |
| 35 | + 0x00 ULCON(线控) / 0x04 UCON(控制) / 0x08 USTAT(状态) |
| 36 | + 0x0C UTXBUF(发送缓冲) / 0x10 URXBUF(接收缓冲) / 0x14 UBRDIV(波特率除数) |
| 37 | + ``` |
| 38 | + |
| 39 | + UART0 基址:`0x3FFD000` → `USTAT0=0x3FFD008`, `UTXBUF0=0x3FFD00C`。 |
| 40 | +* 内存映射寄存器**看起来**像普通内存地址,但**读/写都可能有副作用**:写控制寄存器启动一次操作;读接收寄存器可能设置/清除状态位。优化器若改变访问次数,就可能改变副作用,导致驱动失效。 |
| 41 | + |
| 42 | +### 1.2 UART 发送路径与 TBE 语义 |
| 43 | + |
| 44 | +* `USTAT.TBE`(Transmit Buffer Empty)为 1 时,`UTXBUF` 可写。写入 `UTXBUF` 会触发发送并清零 `TBE`,发送完成后硬件再把 `TBE` 置 1。 |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## 2. 没有 `volatile` 会怎样:编译器会更高效的做错事 |
| 49 | + |
| 50 | +下面这段**故意不加 `volatile`** 的示例很“像对的”,但在优化器眼里全错了: |
| 51 | + |
| 52 | +```cpp |
| 53 | +std::uint32_t &USTAT0 = *reinterpret_cast<std::uint32_t *>(0x03FFD008); |
| 54 | +std::uint32_t &UTXBUF0 = *reinterpret_cast<std::uint32_t *>(0x03FFD00C); |
| 55 | + |
| 56 | +while ((USTAT0 & TBE) == 0) { } // 忙等 |
| 57 | +UTXBUF0 = '\r'; |
| 58 | +while ((USTAT0 & TBE) == 0) { } // 再等 |
| 59 | +UTXBUF0 = '\n'; |
| 60 | +``` |
| 61 | +
|
| 62 | +优化器的推理链条(逐步“合理化”但错误的转化): |
| 63 | +
|
| 64 | +1. `USTAT0` 看起来是普通对象,循环体为空,条件“从未变化”,把 busy loop 变成只判断一次、然后**直接进死循环**或**跳过循环**。 |
| 65 | +2. 第二个 `if`(或第二个 `while`)与第一个条件等价,且“无副作用相关性”,因此**删除第二个条件检查**。 |
| 66 | +3. 两次对 `UTXBUF0` 的写入相邻,第二次覆盖第一次,第一条**可删除**。最终只剩: |
| 67 | +
|
| 68 | + ```cpp |
| 69 | + if ((USTAT0 & TBE) == 0) { for(;;){} } |
| 70 | + UTXBUF0 = '\n'; |
| 71 | + ``` |
| 72 | + |
| 73 | + ——更高效地**做错事**。 |
| 74 | + |
| 75 | +> 关闭整个区域的优化是一个粗暴但“可行”的办法,不过会错失许多有益优化;`volatile` 提供更**精确**的解决方案。 |
| 76 | +
|
| 77 | +--- |
| 78 | + |
| 79 | +## 3. 加上 `volatile`:告诉编译器“别动我” |
| 80 | + |
| 81 | +把寄存器声明成**指向 `volatile` 的引用**: |
| 82 | + |
| 83 | +```cpp |
| 84 | +std::uint32_t volatile &USTAT0 = *reinterpret_cast<std::uint32_t *>(0x03FFD008); |
| 85 | +std::uint32_t volatile &UTXBUF0 = *reinterpret_cast<std::uint32_t *>(0x03FFD00C); |
| 86 | +``` |
| 87 | + |
| 88 | +* 语义(概念层面):对象会“自己变”,即便程序没有显式改它。 |
| 89 | +* 语义(机械层面):**每一次对 `volatile` 对象的读/写都可能有副作用**,编译器**不能省略**,也**不能重排**不同 `volatile` 对象间的访问顺序。 |
| 90 | + |
| 91 | +因此: |
| 92 | + |
| 93 | +```cpp |
| 94 | +while ((USTAT0 & TBE) == 0) { } // 必须真的反复读 |
| 95 | +UTXBUF0 = '\r'; // 不得被搬到循环上方 |
| 96 | +``` |
| 97 | +
|
| 98 | +这两者之间没有显式联系(只是两个地址),但**仅凭“都声明成 `volatile`”**,编译器就必须假定两者副作用相关而**保持顺序**。 |
| 99 | +
|
| 100 | +**但要注意**:`volatile` 的保护是**以“对象”为单位**,不是“代码区域”。编译器仍然可以把**对非 `volatile` 对象的访问**与 `volatile` 访问**重排**(见 §5.1)。 |
| 101 | +
|
| 102 | +--- |
| 103 | +
|
| 104 | +## 4. 声明里 `volatile` 的摆放:别把“指针自己 volatile”搞成“指向的东西 volatile” |
| 105 | +
|
| 106 | +### 4.1 声明结构与关键术语 |
| 107 | +
|
| 108 | +* 每个声明有两部分:**声明说明符**(type / non-type)+ **声明器**(名字与 `* & [] ()` 等)。 |
| 109 | +* `volatile` 和 `const` 都是**类型说明符**,与 `unsigned`、`long` 类似;`static` / `extern` 等是**非类型说明符**。**顺序对编译器不重要**:`unsigned long` 与 `long unsigned` 等价。 |
| 110 | +
|
| 111 | +### 4.2 `volatile` 同时可出现在“类型说明符”与“声明器”里 |
| 112 | +
|
| 113 | +* `volatile int *v[N]` 与 `int volatile *v[N]` ——**指向 `volatile int` 的指针**数组。 |
| 114 | +* `int *volatile v[N]` ——**`volatile` 指针**数组,指向**非 volatile** 的 `int`。这在寄存器场景**很少**需要。 |
| 115 | +
|
| 116 | +### 4.3 牢记一个简单准则:**East const / East volatile** |
| 117 | +
|
| 118 | +> 先写不带 `cv` 的版本,再把 `const/volatile` 放到你想修饰的**类型或运算符的右侧**。 |
| 119 | +
|
| 120 | +例子:把 `x` 声明为“**N 个 const 指针**,每个指向 **volatile uint32_t**” |
| 121 | +
|
| 122 | +```cpp |
| 123 | +uint32_t * x[N]; // 起步 |
| 124 | +uint32_t *const x[N]; // 指针是 const |
| 125 | +uint32_t volatile *const x[N]; // 所指对象是 volatile |
| 126 | +``` |
| 127 | + |
| 128 | +——**`x` 的最终类型**:`uint32_t volatile * const [N]`。 |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## 5. `volatile` 能做什么 & 不能做什么 |
| 133 | + |
| 134 | +### 5.1 顺序与重排 |
| 135 | + |
| 136 | +* **能**:保持所有 `volatile` 访问的**相对顺序**(即便是不同对象)。 |
| 137 | +* **不能**:禁止**非 `volatile`** 访问相对 `volatile` 的重排。典型反例(Eide & Regehr 2008 改编): |
| 138 | + |
| 139 | +```cpp |
| 140 | +bool volatile buffer_ready; |
| 141 | +char buffer[BUF_SIZE]; |
| 142 | + |
| 143 | +void buffer_init() { |
| 144 | + for (int i = 0; i < BUF_SIZE; ++i) buffer[i] = 0; |
| 145 | + buffer_ready = true; // 可能被重排到循环前! |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +编译器可把 `buffer_ready=true` 提前,导致**过早信号**。若把 `buffer` 也声明为 `volatile` 可抑制此类重排,但**代价**是:之后所有对 `buffer` 的使用都失去优化空间。**正确做法**:线程通信请用**同步原语(mutex/semaphore/condvar)**。 |
| 150 | + |
| 151 | +小结:`volatile` **不是**多线程通信工具。标准库/线程库里的同步原语里已经“自带内存栅栏”,可表达所需的可见性与顺序。字幕 Q&A 也特别提到:低层可用**内存屏障**/“同步点”,但大多数应用用高层原语更可靠。 |
| 152 | + |
| 153 | +### 5.2 不保证原子性 |
| 154 | + |
| 155 | +* **`volatile` 只防“错误优化”,不提供原子性或同步。** |
| 156 | + 对 `volatile` 对象的 `++`、`--`、`+=` 等操作在机器层面通常会被拆成“读 → 改 → 写”多步序列,因此**不是不可分割的**。在类型本身**不天然原子**的平台上(例如某些平台的 `double`),并发线程可能观察到**撕裂(torn)**的中间值。 |
| 157 | + |
| 158 | + ```cpp |
| 159 | + double volatile v = 0.0; // 某些平台上 double 非原子 |
| 160 | + // 线程 A |
| 161 | + v = 8.67; |
| 162 | + v = 53.09; // B 可能在此两步之间读到“半写入”的值 |
| 163 | + // 线程 B |
| 164 | + auto x = v; // 可能既不是 8.67 也不是 53.09 |
| 165 | + ``` |
| 166 | + |
| 167 | +* **标准演进提醒的是“误用”而非赋予能力。** |
| 168 | + 由于这些用法(对 `volatile` 的自增/自减/复合赋值)**极易被误解**,它们在 **C++20 被标记为弃用**;考虑到大量存量代码与现实反馈,**C++23 又(部分)撤销了弃用**。但无论“是否弃用”,这些操作的**本质并未改变**——它们**仍不具备原子性**。 |
| 169 | + |
| 170 | +* **正确做法** |
| 171 | + * 需要原子读写/复合操作:使用 `std::atomic<T>` 及合适的内存序。 |
| 172 | + * 只需建立内存序屏障:使用 `atomic_thread_fence(...)`。 |
| 173 | + * 访问硬件寄存器:继续用 `volatile` 来**防止优化器消掉/重排访问**,但**不要**把它当作并发同步工具。 |
| 174 | + |
| 175 | + |
| 176 | +### 5.3 `const volatile` 的组合 |
| 177 | + |
| 178 | +* 实务中很常见:**只读状态寄存器**可以声明为 `const volatile`,以防止误写、又保持“硬件会变”的可见性: |
| 179 | + |
| 180 | + ```cpp |
| 181 | + std::uint32_t const volatile &USTAT0 = *reinterpret_cast<std::uint32_t*>(0x03FFD008); |
| 182 | + ``` |
| 183 | + |
| 184 | +### 5.4 其它不合适的用途 |
| 185 | + |
| 186 | +**Cache 预热不要用 `volatile` 去“强行保留一次读取”**(即防止优化器删除未使用的读)。这会扩大优化屏障、引入不必要的排序/等待,却**不等于**真正的“预取”。 |
| 187 | + |
| 188 | +**更合适的做法**: |
| 189 | + |
| 190 | +* 使用平台提供的**预取内建**(如 `_mm_prefetch` 或编译器内建的 `__builtin_prefetch` 等),它们以“提示”的方式把 cache line 提前带入,而不引入实际的数据依赖或强制读。 |
| 191 | +* 在数据结构层面,结合 C++17 的 `std::hardware_constructive_interference_size` / `std::hardware_destructive_interference_size`,通过**字段分组与填充**减少伪共享、提升命中率。 |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## 6. 当编译器把 `volatile` 搞砸了 |
| 196 | + |
| 197 | +Eide & Regehr(EMSOFT’08)随机程序测试:**5 个系列编译器的 13 个版本**,每个都至少在一种情形**错误处理**了 `volatile`;新版本**未必更少 bug**(GCC 4.2.4 比 4.0.4 还多)。在安全关键系统里,这后果很严重。 |
| 198 | + |
| 199 | +### 6.1 方案一:局部禁用优化 |
| 200 | + |
| 201 | +```cpp |
| 202 | +// 方式 A:GCC 函数级属性 |
| 203 | +void [[gnu::optimize("O0")]] f() { /* ... */ } // O0=最少优化 |
| 204 | +// 方式 B:GCC #pragma |
| 205 | +#pragma GCC push_options |
| 206 | +#pragma GCC optimize("O0") |
| 207 | +void g() { /* ... */ } |
| 208 | +#pragma GCC pop_options |
| 209 | +``` |
| 210 | +
|
| 211 | +> 注意:不同编译器的 pragma 与属性写法不同,且通常是**函数级**生效。 |
| 212 | +
|
| 213 | +### 6.2 方案二:换版本/换编译器 |
| 214 | +
|
| 215 | +同一编译器不同版本的 `volatile` bug 差异显著,且**新**不一定**好**。 |
| 216 | +
|
| 217 | +### 6.3 方案三:非内联函数“包裹”读/写(经典 workaround) |
| 218 | +
|
| 219 | +> Eide & Regehr 的实验显示:把对 `volatile` 的读/写包在**非内联函数**里,能修复他们测到的 **~96%** 的误编译场景(不同编译器与版本)。 |
| 220 | +
|
| 221 | +**普通读/写:** |
| 222 | +
|
| 223 | +```cpp |
| 224 | +int volatile v_int; |
| 225 | +int x = v_int; // 读 |
| 226 | +v_int = 256; // 写 |
| 227 | +``` |
| 228 | + |
| 229 | +**包裹版(关键在“非内联”):** |
| 230 | + |
| 231 | +```cpp |
| 232 | +int vol_read_int(int volatile &vp) { return vp; } |
| 233 | +int volatile &vol_id_int(int volatile &v) { return v; } |
| 234 | + |
| 235 | +int volatile v_int; |
| 236 | +int x = vol_read_int(v_int); // 强化“每次都要真的去读” |
| 237 | +vol_id_int(v_int) = 256; // 强化“这一次写不能合并/删除” |
| 238 | +``` |
| 239 | +
|
| 240 | +**原理** |
| 241 | +编译器在优化**非内联**调用时必须非常保守: |
| 242 | +
|
| 243 | +* 它**看不穿**函数体的副作用,必须**按源代码精确调用**对应次数,不敢合并/删除/跨调用重排访问;这与我们要求的 “`volatile` 每次访问都要发生、且访问之间保持顺序” 高度一致,起到“**冗余保险**”作用。 |
| 244 | +* 调用点天然形成**优化边界**,编译器会假设调用可能读写内存或触发外部副作用,从而不把与之相关的访存上提、下沉或合并。 |
| 245 | +
|
| 246 | +**问题到底可能出在哪里(workaround 要解决的就是这些)** |
| 247 | +历史上,部分优化器曾把“对普通内存合法的变换”**误用**到 `volatile` 上,导致: |
| 248 | +
|
| 249 | +* **删除/合并访问**(DSE/CSE):把“前一次写会被后一次覆盖”当真,删掉前一次;或把多次读缓存成一次寄存器值——而对 `volatile`,**每次访问都必须发生**。 |
| 250 | +* **循环外提**(LICM):把“看似不变”的 `volatile` 读取搬出循环,导致只读一次;设备寄存器值可能变化,这会出错。 |
| 251 | +* **错序/跨语句重排**:把 `volatile` 访问与其他访问错误换序,改变硬件交互因果。对不同 `volatile` 对象之间也必须保持顺序。 |
| 252 | +* **向量化/跨基本块合并**:合并访存或调整时序,违背 “每次都要发生且按序” 的要求。上述问题均在论文与讲稿中有例示或讨论。 |
| 253 | +
|
| 254 | +**实践注意事项** |
| 255 | +
|
| 256 | +* **严禁被“自动内联”偷走护城河**:模板版本或较高优化级别下,编译器可能**自动内联**这些小函数——一旦内联,优化边界消失,等同没加。解决:给函数(含模板实例)显式加 `[[gnu::noinline]]` / 对应编译器的 `noinline` 标记,并留意 LTO/IPO 设置。讲者在 GNU ARM Embedded Toolchain 10.2.1 上就遇到过自动内联,最终用 `[[gnu::noinline]]` 压住。 |
| 257 | +* **它是工程折中,不是语言保证**:**~96%** 是经验数据,而非标准承诺;若仍遇到工具链缺陷,可配合“局部降优化级(O0)”“更换编译器/版本”等手段联合使用。 |
| 258 | +* **性能权衡**:非内联调用在热路径/ISR 中有成本。可仅在特定编译器/配置下启用包裹,或将包裹函数放在对正确性至关重要、但调用频率相对适中的路径上。 |
| 259 | +
|
| 260 | +小结:问题出在优化器把“对普通内存安全的变换”(删除/合并/外提/重排)套在了 `volatile` 上;“非内联函数包裹”利用**调用边界的保守性**,强制保留**每一次**访问并**按原序**执行,从而在实践中大幅降低误编译风险。 |
| 261 | +
|
| 262 | +
|
| 263 | +### 用于 UART 的实战改造: |
| 264 | +
|
| 265 | +```cpp |
| 266 | +std::uint32_t vol_read_u32(std::uint32_t volatile &v) { return v; } |
| 267 | +std::uint32_t volatile &vol_id_u32(std::uint32_t volatile &v) { return v; } |
| 268 | +
|
| 269 | +std::uint32_t volatile &USTAT0 = *reinterpret_cast<std::uint32_t *>(0x03FFD008); |
| 270 | +std::uint32_t volatile &UTXBUF0 = *reinterpret_cast<std::uint32_t *>(0x03FFD00C); |
| 271 | +
|
| 272 | +while ((vol_read_u32(USTAT0) & TBE) == 0) { } |
| 273 | +vol_id_u32(UTXBUF0) = '\r'; |
| 274 | +``` |
| 275 | + |
| 276 | +实测有效;**模板版**易被 -O1 起自动内联,需加 `[[gnu::noinline]]` 抑制(MSVC 可用 `__declspec(noinline)`,Clang/GCC 也支持 `__attribute__((noinline))`)。务必确认**没被内联**。 |
| 277 | + |
| 278 | +--- |
| 279 | + |
| 280 | +## 7. 实用清单(Checklist) |
| 281 | + |
| 282 | +1. **硬件寄存器**:指向寄存器的对象一律 `volatile`;只读寄存器用 `const volatile`。 |
| 283 | +2. **East volatile**:把 `volatile` 放在你想修饰的**类型/运算符右边**,别把“指针 volatile”写成“指向对象 volatile”或反过来。 |
| 284 | +3. **线程通信**:**不要**用 `volatile`;请用 `mutex / semaphore / condvar / std::atomic(*) / fence`。 |
| 285 | +4. **原子性**:`volatile` ≠ 原子;增量/复合赋值可能数据竞争。 |
| 286 | +5. **遇到编译器问题**: |
| 287 | + * 局部关优化(属性/pragma); |
| 288 | + * 换版本/换编译器; |
| 289 | + * **非内联函数包裹**(并确认没被内联)。 |
| 290 | +6. **缓存预取**: 别靠 `volatile`,用 `_mm_prefetch` 等内建;缓存行布局可参考 C++17 的“硬件构造/破坏性干涉尺寸”常量。 |
| 291 | + * `std::hardware_constructive_interference_size`:构造性干涉尺寸。把经常一起访问的数据项放在不超过这个尺寸的距离内,倾向于落在同一 cache line,提升命中(“一起热”)。 |
| 292 | + * `std::hardware_destructive_interference_size`:破坏性干涉尺寸。把可能被不同线程同时写的对象间隔至少这么大,尽量不落在同一 cache line,避免伪共享(false sharing,“互相拖累”)。 |
| 293 | + |
| 294 | +--- |
| 295 | + |
| 296 | +## 8. 关键代码片段汇总 |
| 297 | + |
| 298 | +* **错误示例(无 `volatile`)→ 被优化坏**:忙等、两次写入被合并,仅发送 `'\n'`(详见 §2)。 |
| 299 | +* **正确示例(有 `volatile`)**: |
| 300 | + |
| 301 | + ```cpp |
| 302 | + std::uint32_t volatile &USTAT0 = *reinterpret_cast<std::uint32_t*>(0x03FFD008); |
| 303 | + std::uint32_t volatile &UTXBUF0 = *reinterpret_cast<std::uint32_t*>(0x03FFD00C); |
| 304 | + while ((USTAT0 & TBE) == 0) { } |
| 305 | + UTXBUF0 = c; |
| 306 | + ``` |
| 307 | +
|
| 308 | +
|
| 309 | +* **`const volatile` 状态寄存器**: |
| 310 | +
|
| 311 | + ```cpp |
| 312 | + std::uint32_t const volatile &USTAT0 = *reinterpret_cast<std::uint32_t*>(0x03FFD008); |
| 313 | + ``` |
| 314 | + |
| 315 | +* **East volatile 模板**: |
| 316 | + |
| 317 | + ```cpp |
| 318 | + uint32_t volatile *const x[N]; // “N 个 const 指针,指向 volatile uint32_t” |
| 319 | + ``` |
| 320 | + |
| 321 | + |
| 322 | +* **非内联包裹读/写**: |
| 323 | + |
| 324 | + ```cpp |
| 325 | + int vol_read_int(int volatile &vp) { return vp; } |
| 326 | + int volatile &vol_id_int(int volatile &v) { return v; } |
| 327 | + ``` |
| 328 | +
|
| 329 | + (模板版要 `[[gnu::noinline]]` 或对应编译器的 noinline) |
| 330 | +
|
| 331 | +* **GCC 局部关优化**: |
| 332 | +
|
| 333 | + ```cpp |
| 334 | + void [[gnu::optimize("O0")]] f() { /* ... */ } |
| 335 | +
|
| 336 | + #pragma GCC push_options |
| 337 | + #pragma GCC optimize("O0") |
| 338 | + void g() { /* ... */ } |
| 339 | + #pragma GCC pop_options |
| 340 | + ``` |
| 341 | + |
| 342 | +* **缓存预取**: |
| 343 | + |
| 344 | + ```cpp |
| 345 | + #include <atomic> |
| 346 | + #include <new> |
| 347 | + |
| 348 | + // 把两个热点计数器拆到不同的 cache line,避免伪共享 |
| 349 | + struct alignas(std::hardware_destructive_interference_size) Counter { |
| 350 | + std::atomic<long> value{0}; |
| 351 | + }; |
| 352 | + |
| 353 | + struct HotPair { |
| 354 | + // 把经常一起读的字段放在“构造性干涉尺寸”内,利于同线命中 |
| 355 | + alignas(std::hardware_constructive_interference_size) int a; |
| 356 | + int b; |
| 357 | + }; |
| 358 | + |
| 359 | + void touch(Counter& c1, Counter& c2, const HotPair* hp) { |
| 360 | + // 平台预取(示例:x86 SSE/AVX),编译器/平台不同可用 __builtin_prefetch |
| 361 | + // _mm_prefetch(reinterpret_cast<const char*>(hp), _MM_HINT_T0); |
| 362 | + |
| 363 | + c1.value.fetch_add(1, std::memory_order_relaxed); |
| 364 | + c2.value.fetch_add(1, std::memory_order_relaxed); |
| 365 | + int t = hp->a + hp->b; (void)t; |
| 366 | + } |
| 367 | + ``` |
| 368 | +
|
| 369 | +--- |
| 370 | +
|
| 371 | +## 9. 压轴总结(Takeaways) |
| 372 | +
|
| 373 | +* `volatile` 的**唯一核心价值**:告诉编译器“这里的读写可能有副作用,请**别省略、别合并、别重排**这些访问”。它**不会**解决线程可见性/顺序/原子性问题。 |
| 374 | +* **多线程通信**:请用同步原语/原子库/内存序;`volatile` 不适用。 |
| 375 | +* **工程上**:若你怀疑编译器把 `volatile` 搞坏了,优先考虑**局部关优化 / 换编译器版本 / 非内联包裹**三种方法,并**确认没被内联**。 |
| 376 | +
|
| 377 | +
|
| 378 | +[1]: https://www.youtube.com/watch?v=GeblxEQIPFM&list=PLHTh1InhhwT6U7t1yP2K8AtTEKmcM3XU_&index=2&t=2s |
| 379 | +[2]: https://github.com/CppCon/CppCon2024/blob/main/Presentations/What_Volatile_Means_(and_Doesn't_Mean).pdf |
0 commit comments