面试必杀:什么是‘强一致性内存模型’?解析 C++ atomic 对 CPU 指令集的映射

面试必杀:深入理解强一致性内存模型与 C++ Atomic 对 CPU 指令集的映射

各位技术同仁,欢迎来到今天的专题讲座。在现代多核处理器和并发编程的时代,我们经常面临一个核心挑战:如何确保共享数据的正确性与可见性。这不仅仅是编写正确的互斥锁那么简单,它深入到了硬件与编译器的运作机制,以及它们如何协同或对抗程序员的预期。今天,我们将聚焦于“强一致性内存模型”,并详细解析 C++ std::atomic 如何在这一复杂语境下,通过与底层 CPU 指令集的映射,帮助我们构建健壮的并发程序。

第一章:并发编程的幻象与内存模型的诞生

在单线程编程中,代码的执行顺序似乎总是显而易见的:指令按照它们在程序中出现的顺序一条接一条地执行。然而,一旦我们踏入多线程的世界,这种直观的“顺序执行”幻象便会迅速破灭。为了榨取极致的性能,现代 CPU 和编译器会进行大量的优化:

  1. 指令重排 (Instruction Reordering):CPU 会为了更好地利用流水线、隐藏内存延迟而改变指令的实际执行顺序,只要不改变单个线程内的可见结果。
  2. 缓存 (Caches):每个核心都有自己的高速缓存(L1、L2),以及共享缓存(L3)。数据写入时可能只更新了本地缓存,而没有立即写入主内存,导致其他核心无法立即看到最新值。
  3. 写缓冲器 (Store Buffers):CPU 核心在执行写操作时,可能不会立即将数据写入缓存,而是先放入写缓冲器,然后继续执行后续指令。
  4. 编译器优化 (Compiler Optimizations):编译器在生成机器码时,也会为了性能考虑而重排指令,甚至消除看似冗余的内存访问,只要它认为这些优化不影响单线程语义。

这些优化虽然极大地提升了程序速度,但对于多线程程序而言,它们引入了“内存可见性”和“操作顺序”的混沌。一个线程写入的数据,另一个线程可能看不到;一个线程的操作序列,在另一个线程看来可能是乱序的。

为了驯服这种混沌,我们引入了“内存模型”的概念。内存模型 (Memory Model) 是一套契约,它规定了在多线程环境下,一个线程对内存的写入何时以及以何种顺序对其他线程可见。它定义了编译器和硬件可以执行的重排类型,以及程序员必须使用哪些同步机制来强制特定的顺序和可见性。

第二章:理想与现实:顺序一致性与松弛内存模型

在内存模型的谱系中,最强大、最直观的理想状态被称为“顺序一致性” (Sequential Consistency, SC)。

2.1 顺序一致性 (Sequential Consistency)

定义:一个内存模型是顺序一致的,如果所有操作(包括原子操作和非原子操作)看起来都按照程序指定的顺序执行,并且所有处理器都以相同的全局顺序看到所有操作。

简而言之,顺序一致性提供了最简单的并发编程心智模型:

  1. 程序顺序 (Program Order):单个线程内部的所有操作都按照其在源代码中出现的顺序执行。
  2. 全局顺序 (Global Order):所有线程的操作可以被交错成一个单一的、全局的执行序列。这个序列中的每个线程的操作都符合其程序顺序。

假设我们有以下两个线程:

// 线程 A
void thread_A() {
    X = 1; // 操作 A1
    Y = 1; // 操作 A2
}

// 线程 B
void thread_B() {
    while (Y != 1); // 操作 B1
    assert(X == 1); // 操作 B2
}

在顺序一致性模型下,如果 Y = 1 被线程 B 看到,那么 X = 1 也必然已经被线程 B 看到。因为 A1 和 A2 必须按照 A1 -> A2 的顺序执行并对所有线程可见。如果 B1 观察到 A2 的结果,那么 A1 的结果也必然已经完成。

优点:编程心智模型简单,易于理解和推理。
缺点:实现成本极高。为了保证所有操作的全局顺序,硬件必须频繁地刷新缓存、等待写缓冲器清空,并插入昂贵的内存屏障,这会极大地限制 CPU 的并行优化能力。因此,现代处理器几乎都不直接提供硬件层面的全局顺序一致性。

2.2 松弛内存模型 (Relaxed Memory Models)

由于顺序一致性的性能开销,大多数现代处理器和编程语言都采用了“松弛内存模型”。这些模型允许一定程度的指令重排和内存访问重排,以换取性能。

这意味着,默认情况下,在没有显式同步的情况下,编译器和 CPU 可以随意重排内存操作。这导致了臭名昭著的“数据竞争” (Data Race) 问题。

数据竞争:当两个或多个线程并发访问同一个内存位置,并且至少有一个是写入操作,同时这些访问中至少有一个不是原子操作时,就发生了数据竞争。C++ 标准明确规定,数据竞争会导致未定义行为 (Undefined Behavior)。这意味着你的程序可能崩溃、产生错误结果,或者在不同运行环境下表现不一。

为了在松弛内存模型下编写正确的并发程序,C++ 引入了 std::atomic 类型和一系列内存顺序 (memory order) 选项,允许程序员精确地控制原子操作的可见性和顺序保证。

第三章:C++ 内存模型与 std::atomic

C++11 引入了强大的内存模型,它基于“释放-获取” (Release-Acquire) 语义,并提供了一套精细的工具来管理并发操作的顺序和可见性。核心工具就是 std::atomic 模板类。

3.1 std::atomic 的核心作用

std::atomic<T> 类型保证了对 T 类型对象的读、写、读-修改-写操作(如 fetch_add, compare_exchange_weak 等)都是原子性的。这意味着这些操作要么完全完成,要么完全不发生,不会被其他线程的操作打断。

但仅仅是原子性还不够。原子性只解决了“操作本身是不可分割的”问题,而没有解决“操作的顺序和可见性”问题。这正是内存顺序发挥作用的地方。

3.2 C++ 中的内存顺序 (std::memory_order)

std::memory_order 枚举提供了六种内存顺序,它们决定了原子操作如何与程序中其他内存操作进行排序:

  1. std::memory_order_relaxed
  2. std::memory_order_consume (已弃用/不推荐直接使用)
  3. std::memory_order_acquire
  4. std::memory_order_release
  5. std::memory_order_acq_rel
  6. std::memory_order_seq_cst

这些内存顺序从最松弛到最严格,提供了不同的性能与语义权衡。

3.2.1 std::memory_order_relaxed (最弱保证)

语义:只保证操作本身的原子性,不提供任何内存顺序保证。编译器和 CPU 可以随意重排这些操作,甚至可以将其与非原子操作交叉重排。

用途:适用于那些不需要同步其他内存操作,只关心原子计数或简单标志的场景。

示例

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter(0);

void increment_relaxed() {
    for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 仅保证原子性
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_relaxed);
    }
    for (auto& t : threads) {
        t.join();
    }
    // 最终结果将是 10 * 100000 = 1000000,因为fetch_add是原子的
    // 但不能保证在 counter.fetch_add(1) 之前的非原子操作一定在之前完成可见
    // 或在之后的非原子操作一定在之后开始
    std::cout << "Final counter (relaxed): " << counter.load(std::memory_order_relaxed) << std::endl;
    return 0;
}
3.2.2 std::memory_order_acquirestd::memory_order_release (释放-获取语义)

这是 C++ 内存模型中最核心的同步机制,提供了比 relaxed 更强的顺序保证,同时通常比 seq_cst 更高效。

  • std::memory_order_release (写操作)

    • 语义:一个线程在执行 release 操作时,它之前的所有内存写操作都必须在 release 操作完成之前完成,并且对其他线程可见。它阻止了 release 操作之前的读写操作被重排到 release 操作之后
    • “释放”了之前的所有内存修改。
  • std::memory_order_acquire (读操作)

    • 语义:一个线程在执行 acquire 操作时,它之后的所有内存读操作都必须在 acquire 操作完成之后完成。它阻止了 acquire 操作之后的读写操作被重排到 acquire 操作之前
    • “获取”了由另一个线程释放的内存修改,并保证这些修改在 acquire 之后的所有操作中可见。

“同步于 (synchronizes-with)” 关系
如果线程 A 对原子变量 X 执行了一个 release 操作,而线程 B 对同一个原子变量 X 执行了一个 acquire 操作并读取到了 A 写入的值(或后续由 A 写入的值),那么就建立了“同步于”关系。
这意味着:

  1. 线程 A 在 release 操作之前的所有内存写入,对线程 B 在 acquire 操作之后的所有内存读取都可见。
  2. 线程 A 的 release 操作发生在线程 B 的 acquire 操作之前。

示例:经典的生产者-消费者模型

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <string>

std::atomic<bool> ready(false);
std::string data; // 共享数据

void producer() {
    data = "Hello, C++ Memory Model!"; // 1. 非原子写
    ready.store(true, std::memory_order_release); // 2. 释放操作
    std::cout << "Producer: Data sent, ready flag set." << std::endl;
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) { // 3. 获取操作
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    // 4. 读取数据
    std::cout << "Consumer: Received data: " << data << std::endl;
    // 由于 ready.store(true, release) 同步于 ready.load(acquire)
    // 故 "Hello, C++ Memory Model!" 的写入操作 (1) 在 consumer 线程中 (4) 读取时是可见的。
    // 如果没有 memory_order_release/acquire, data 的写入可能被重排到 ready.store 之后,
    // 或者 data 的读取被重排到 ready.load 之前,导致consumer看到空字符串或垃圾值。
}

int main() {
    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();
    return 0;
}
3.2.3 std::memory_order_acq_rel (读-修改-写操作)

语义:用于读-修改-写 (RMW) 原子操作(如 fetch_add, compare_exchange_weak/strong)既要保证读操作的 acquire 语义,又要保证写操作的 release 语义。

用途:常用于实现自旋锁或无锁数据结构中的更新操作。

示例:原子计数器,其每次更新都必须可见并同步于其他线程。

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> shared_counter(0);

void increment_acq_rel() {
    for (int i = 0; i < 100000; ++i) {
        // fetch_add 是一个RMW操作,这里使用acq_rel
        // 它会获取到当前值(acquire语义),然后更新(release语义)
        shared_counter.fetch_add(1, std::memory_order_acq_rel);
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_acq_rel);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Final counter (acq_rel): " << shared_counter.load(std::memory_order_acquire) << std::endl;
    return 0;
    // load(acquire) 确保能看到所有 release 操作之后的值
}
3.2.4 std::memory_order_seq_cst (最强保证:顺序一致性)

语义:这是最强的内存顺序,它为所有标记为 seq_cst 的原子操作提供全局的顺序一致性。这意味着所有线程都会以相同的、单一的、总体的顺序看到所有 seq_cst 操作。它隐式地包含了 acquirerelease 的所有保证,并且更进一步,强制在所有 seq_cst 操作之间建立一个全局的偏序。

用途

  1. 当你需要最简单的心智模型,或者对内存模型的细节不确定时。
  2. 当你的算法确实依赖于所有线程看到一个统一的全局操作顺序时。

缺点:性能开销最大,因为它通常需要更强的内存屏障(如 x86 上的 mfencelock 前缀指令),会清空写缓冲器并确保缓存同步。

示例
std::atomic 默认的内存顺序就是 std::memory_order_seq_cst

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<bool> x(false);
std::atomic<bool> y(false);
std::atomic<int> z(0);

void thread_1() {
    x.store(true, std::memory_order_seq_cst); // S1
}

void thread_2() {
    y.store(true, std::memory_order_seq_cst); // S2
}

void thread_3() {
    while (!x.load(std::memory_order_seq_cst)); // L1
    if (y.load(std::memory_order_seq_cst)) {    // L2
        z.fetch_add(1, std::memory_order_seq_cst); // A1
    }
}

void thread_4() {
    while (!y.load(std::memory_order_seq_cst)); // L3
    if (x.load(std::memory_order_seq_cst)) {    // L4
        z.fetch_add(1, std::memory_order_seq_cst); // A2
    }
}

int main() {
    std::vector<std::thread> threads;
    threads.emplace_back(thread_1);
    threads.emplace_back(thread_2);
    threads.emplace_back(thread_3);
    threads.emplace_back(thread_4);

    for (auto& t : threads) {
        t.join();
    }

    // 在seq_cst下,z的值总是1或2。
    // 不可能出现 z==0 的情况,因为如果 L1 看到 S1,且 L3 看到 S2,
    // 那么 S1 和 S2 之间有一个全局顺序。
    // 如果 S1 先于 S2,则 L1 看到 S1 之后,L2 读取 Y 必然会看到 S2 的结果。
    // 如果 S2 先于 S1,则 L3 看到 S2 之后,L4 读取 X 必然会看到 S1 的结果。
    // 这保证了至少有一个 if 条件会为真。
    std::cout << "Final z (seq_cst): " << z.load(std::memory_order_seq_cst) << std::endl;
    return 0;
}

注意:如果将上述示例中的 seq_cst 替换为 acquire/release,则 z 的最终值可能是 0。这是因为 acquire/release 仅建立了成对的同步关系,但不能保证所有 acquire/release 操作之间有一个全局的总序。

3.2.5 std::atomic_thread_fence

除了对原子操作指定内存顺序,C++ 还提供了 std::atomic_thread_fence 来插入独立的内存屏障。

语义std::atomic_thread_fence(std::memory_order_xxx) 会在当前线程中建立一个内存屏障,但它不涉及任何原子变量的读写。它通常用于与非原子操作或 relaxed 原子操作结合使用,以强制特定的内存顺序。

  • std::atomic_thread_fence(std::memory_order_acquire):所有屏障之后的读操作都必须在屏障之后完成。
  • std::atomic_thread_fence(std::memory_order_release):所有屏障之前的写操作都必须在屏障之前完成。
  • std::atomic_thread_fence(std::memory_order_acq_rel):同时具有 acquirerelease 语义。
  • std::atomic_thread_fence(std::memory_order_seq_cst):最强的屏障,提供全局顺序一致性。

示例:强制非原子写入的可见性

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

int non_atomic_data = 0;
std::atomic<bool> flag(false);

void producer_fence() {
    non_atomic_data = 42; // 非原子写
    std::atomic_thread_fence(std::memory_order_release); // 释放屏障
    flag.store(true, std::memory_order_relaxed); // 宽松存储 flag
}

void consumer_fence() {
    while (!flag.load(std::memory_order_relaxed)); // 宽松加载 flag
    std::atomic_thread_fence(std::memory_order_acquire); // 获取屏障
    // 由于 release fence 和 acquire fence 形成了同步关系
    // non_atomic_data 的写入现在对 consumer 可见
    std::cout << "Consumer: non_atomic_data = " << non_atomic_data << std::endl;
}

int main() {
    std::thread p(producer_fence);
    std::thread c(consumer_fence);
    p.join();
    c.join();
    return 0;
}

第四章:C++ Atomic 到 CPU 指令集的映射 (以 x86-64 为例)

C++ 内存模型是一个抽象概念,它需要编译器将其映射到底层硬件的实际行为。不同的 CPU 架构有不同的内存模型强度和指令集来强制内存排序。理解这种映射对于优化性能和深入调试至关重要。

我们将以目前最常见的 x86-64 架构为例进行解析。x86-64 的内存模型相对于 ARM 或 POWER 等架构来说是比较强的。它提供了一些默认的内存排序保证:

  • 程序顺序中的写操作不会被重排:一个核心对同一地址的写操作,不会在程序顺序上被重排。
  • 写操作的原子性:对对齐的、自然大小的内存(通常是 8 字节)的单次写操作是原子的。
  • 写操作的可见性:一个核心的写操作,在其他核心上看到的顺序与它实际发生的顺序一致。
  • Store Forwarding:一个核心写入的数据可以立即被该核心的后续读取操作看到,即使数据还在写缓冲器中。

然而,即使 x86-64 内存模型相对较强,仍然可能发生以下重排:

  • Load-Load 重排Load1; Load2 可以变成 Load2; Load1
  • Store-Store 重排Store1; Store2 可以变成 Store2; Store1(通过写缓冲器)。
  • Load-Store 重排Load1; Store1 可以变成 Store1; Load1

Store-Load 重排 (Store1; Load1 变成 Load1; Store1) 是被严格禁止的。这意味着一个写操作不能越过一个读操作。

为了强制更严格的内存顺序,x86-64 提供了特殊的指令:

  • LOCK 前缀:可以加在某些指令(如 ADD, XCHG, CMPXCHG 等)前面。它会锁定总线,使该操作成为原子操作,并且作为一个全内存屏障 (Full Memory Barrier),阻止所有类型(Load-Load, Store-Store, Load-Store, Store-Load)的重排越过它。它清空写缓冲器并确保缓存一致性消息被处理。
  • MFENCE (Memory Fence):一个全内存屏障,等同于 LOCK 前缀提供的屏障功能,但它不执行任何数据操作,仅用于强制内存排序。
  • LFENCE (Load Fence):阻止所有在其之后的 Load 操作被重排到其之前
  • SFENCE (Store Fence):阻止所有在其之前的 Store 操作被重排到其之后

现在,我们来看看 C++ std::atomic 操作如何映射到 x86-64 指令:

| C++ std::atomic 操作类型 | std::memory_order | 典型的 x86-64 指令序列 ### 第五章:C++ Atomics 与其在 x86-64 上的具体映射

我们将更详细地探讨 C++ std::atomic 的不同内存顺序如何在 x86-64 架构上生成对应的机器指令。理解这些映射对于预测性能和解决某些复杂的并发问题至关重要。

首先,回顾一下 x86-64 内存模型的一些关键特性:

  • 强写序 (Strong Write Ordering):一个处理器核心的写操作,被其他处理器核心看到的顺序,与它被第一个核心发出的顺序一致。
  • 写缓冲器 (Store Buffer):每个核心都有一个写缓冲器,写操作通常先进入缓冲器,然后异步地写入缓存。这可能导致本核心的写操作对其他核心不可见,直到缓冲器被清空。
  • 缓存一致性协议 (Cache Coherence Protocol):如 MESI/MOESI,确保了当一个核心修改一个缓存行时,其他核心的对应缓存行会失效或更新。

5.1 std::memory_order_relaxed

  • 语义:只保证操作原子性。无任何内存顺序保证。
  • x86-64 映射
    • load(std::memory_order_relaxed):通常映射为普通的 MOV 指令。x86-64 的 MOV 加载本身就是原子的(对于自然对齐的字大小),并且没有重排限制(但 Load-Load 仍可能被重排)。
    • store(value, std::memory_order_relaxed):通常映射为普通的 MOV 指令。x86-64 的 MOV 存储也是原子的,并且写操作对本核心是强序的,但对其他核心的可见性取决于缓存和写缓冲器。
    • fetch_add, compare_exchange_weak (RMW 操作):通常映射为带有 LOCK 前缀的指令,例如 LOCK XADD (原子加) 或 LOCK CMPXCHG (比较并交换)。即使是 relaxed 语义,RMW 操作在 x86-64 上也需要 LOCK 前缀来保证原子性,而 LOCK 前缀本身就包含了全内存屏障的效果。 这意味着在 x86-64 上,relaxed 的 RMW 操作实际上比其名义上的语义更强。这是一个重要的实现细节。

代码示例 (AT&T 汇编风格,仅供说明概念)

; relaxed_load:
mov     %rax, [rdi]     ; Load from memory at rdi into rax (e.g., x.load(memory_order_relaxed))

; relaxed_store:
mov     [rdi], %rax     ; Store rax into memory at rdi (e.g., x.store(val, memory_order_relaxed))

; relaxed_fetch_add (RMW):
lock xadd %eax, [rdi]   ; Atomically add rax to memory at rdi, store old value in rax
                        ; (e.g., x.fetch_add(val, memory_order_relaxed))
                        ; Note: 'lock' implies full fence on x86-64

5.2 std::memory_order_acquire

  • 语义:阻止屏障之后的读操作被重排到屏障之前
  • x86-64 映射
    • load(std::memory_order_acquire):在 x86-64 上,通常也映射为普通的 MOV 指令。这是因为 x86-64 的处理器设计已经提供了足够的强序保证,即它不会将一个加载操作后面的加载或存储操作重排到这个加载操作之前。换句话说,LFENCE 并不是严格必要的,除非有非常特殊的编译器优化需要阻止。
    • compare_exchange_weak (RMW 操作):和 relaxed RMW 一样,使用 LOCK CMPXCHGLOCK 前缀提供了全内存屏障,满足 acquire 语义。

为什么 MOV 通常就够了? x86-64 架构保证了 Load 不能越过 Load 或 Store。因此,acquire 语义(禁止后续读操作重排到当前读操作之前)在大多数情况下由硬件的默认行为提供。

; acquire_load:
mov     %rax, [rdi]     ; Load from memory at rdi into rax (e.g., x.load(memory_order_acquire))
                        ; No explicit fence usually needed on x86-64 for acquire loads.

5.3 std::memory_order_release

  • 语义:阻止屏障之前的写操作被重排到屏障之后
  • x86-64 映射
    • store(value, std::memory_order_release)
      • 如果是非 RMW 存储:通常映射为 MOV 指令。在某些情况下,编译器可能会在 MOV 之后插入一个 SFENCE 指令(不太常见,因为 x86 的写缓冲器通常通过其他机制被清除,例如 LOCK 指令或 MFENCE)。但更常见的是,release 语义在 x86-64 上是隐式满足的,因为 x86-64 保证了 Store 不会越过 Store,并且 Store 不会越过 Load。最重要的是,LOCK 前缀指令本身就具有 release 语义,如果后续有 LOCK 操作,则 MOV 之前的所有写操作都将可见。
      • 如果紧接着是一个 LOCK 前缀的指令(如 xchglock add 等):那么 LOCK 指令会提供全内存屏障,使得之前的 MOV 存储具有 release 语义。
    • compare_exchange_weak (RMW 操作):使用 LOCK CMPXCHGLOCK 前缀提供了全内存屏障,满足 release 语义。

为什么 MOV 通常就够了? x86-64 架构保证了 Store 不能越过 Store。当一个 Store 操作完成时,其结果最终会通过缓存一致性协议传播出去。release 语义的核心是确保之前的写操作在 release 操作完成前对其他处理器可见。在 x86-64 上,写入写缓冲器的数据最终会写入缓存。如果 release 操作本身是一个 LOCK 前缀的 RMW 操作,则它会清空写缓冲器并充当一个全屏障。如果只是一个普通存储,通常不需要额外的 SFENCE,因为写缓冲器最终会清空,并且 x86-64 的写序保证。

; release_store:
mov     [rdi], %rax     ; Store rax into memory at rdi (e.g., x.store(val, memory_order_release))
                        ; No explicit fence usually needed on x86-64 for release stores.
                        ; The strong write ordering and implicit flushing via LOCK operations
                        ; often suffice.

5.4 std::memory_order_acq_rel

  • 语义:同时具有 acquirerelease 的语义。用于读-修改-写 (RMW) 操作。
  • x86-64 映射
    • 所有 acq_rel RMW 操作(如 fetch_add, compare_exchange_weak/strong)都映射为带有 LOCK 前缀的指令,例如 LOCK XADDLOCK CMPXCHGLOCK 前缀本身就是一个全内存屏障,因此它自然地满足了 acquirerelease 的所有语义。
; acq_rel_fetch_add:
lock xadd %eax, [rdi]   ; Atomically add rax to memory at rdi, store old value in rax
                        ; (e.g., x.fetch_add(val, memory_order_acq_rel))
                        ; 'lock' implies full fence on x86-64

5.5 std::memory_order_seq_cst (强一致性)

  • 语义:提供全局的顺序一致性。这是最强的保证,它强制所有 seq_cst 操作之间存在一个单一的总序。
  • x86-64 映射
    • load(std::memory_order_seq_cst):通常映射为 MOV 指令,后面紧跟一个 MFENCE 指令。MFENCE 确保所有之前的 Load 和 Store 都已完成,并且所有后续的 Load 和 Store 都不会被重排到 MFENCE 之前。
    • store(value, std::memory_order_seq_cst):通常映射为 MFENCE 指令,后面紧跟一个 MOV 指令。MFENCE 确保所有之前的 Load 和 Store 都已完成,并且当前 MOV 不会重排到 MFENCE 之前。
    • compare_exchange_weak (RMW 操作):映射为带有 LOCK 前缀的指令,例如 LOCK CMPXCHGLOCK 前缀本身就是全内存屏障。但在某些情况下,编译器可能会在 LOCK 指令之后再插入一个 MFENCE,以确保所有 seq_cst 操作的全局总序,尽管 LOCK 本身已经很强了。这取决于具体的编译器和上下文。
; seq_cst_load:
mov     %rax, [rdi]     ; Load
mfence                  ; Full memory barrier (e.g., x.load(memory_order_seq_cst))

; seq_cst_store:
mfence                  ; Full memory barrier
mov     [rdi], %rax     ; Store (e.g., x.store(val, memory_order_seq_cst))

; seq_cst_fetch_add (RMW):
lock xadd %eax, [rdi]   ; Atomically add rax to memory at rdi
mfence                  ; Potentially an additional mfence for strict seq_cst (compiler dependent)

5.6 std::atomic_thread_fence

  • x86-64 映射
    • std::atomic_thread_fence(std::memory_order_acquire):通常不需要显式指令,因为 x86-64 的 Load 顺序保证已经很强。
    • std::atomic_thread_fence(std::memory_order_release):通常不需要显式指令,因为 x86-64 的 Store 顺序保证已经很强。
    • std::atomic_thread_fence(std::memory_order_acq_rel):通常映射为 MFENCE
    • std::atomic_thread_fence(std::memory_order_seq_cst):映射为 `

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注