动态

详情 返回 返回

從彙編看內存序:C++ 內存模型在 Intel 架構下到底做了什麼 - 动态 详情

一、引言

在多線程程序中,C++ 內存模型定義了跨線程訪問共享變量時的行為保證。
它規定了不同操作之間的 可見性 與 順序性,通過諸如 memory_order_relaxed、memory_order_release、memory_order_acquire、memory_order_seq_cst 等語義,讓開發者能夠在性能與正確性之間做出權衡。
然而,C++ 的內存模型只是一個 抽象規範。真正執行這些語義的,是底層的硬件內存模型。
不同架構的 CPU(如 Intel x86 與 ARM)有着不同的內存一致性保證:

  • ARM的內存模型較弱,需要頻繁使用內存屏障;
  • Intel 的內存模型天生較強,很多語義在硬件上就已經保證。

這篇文章就從彙編層面出發,看看:C++ 的各種內存序語義,在 Intel 處理器下 到底做了什麼?

二、Intel 的天然強內存序(TSO)

TSO(Total Store Order) 模型。

Intel 的處理器自 P6 以來採用了所謂的 TSO 模型。
根據《Intel® 64 and IA-32 Architectures Software Developer’s Manual》10.2.2節的描述:
memory ordering 1
memory ordering 2
關注公眾號“Hankin-Liu的技術研究室”,回覆“intel manual"可獲得此INTEL官方手冊下載鏈接。

其核心特徵可以概括如下:

  • 讀不會亂序:
  • 寫不會越過更早的讀:
  • 寫與寫之間按順序執行(除非是特殊的 streaming store 或字符串操作):
  • 讀可能越過舊的寫(不同地址):即允許「Read after Write」亂序,但同地址不行:
  • 跨核可見性保證:同一 CPU 核發出的寫,在所有其他核上可見的順序一致:
  • 鎖指令形成全局順序(total order):

這意味着:Intel 架構在多核環境下天然具備較強的內存可見性順序。

🧩 TSO 模型的關鍵:Store Buffer + Load Forwarding

Intel 的強一致性來自兩個機制的權衡:

  1. Store Buffer
    寫操作先進入緩衝區,異步刷入緩存。這樣寫線程不會被內存訪問阻塞。
  2. Store-to-Load Forwarding
    當程序緊接着讀同一個地址,CPU 會直接從 store buffer 裏讀,避免看到舊值。

這種機制讓讀寫看似“按程序順序執行”,但實際上 CPU 在後台亂序執行。
Intel 的設計理念是:儘可能維持編程語義上的順序,而不是強制所有指令完全按順序執行。

三、從彙編看 C++ 內存序的真實區別

memory_order_relaxed

程序示例:

#include <atomic>
#include <iostream>
std::atomic<int> ready{};

__attribute__ ((noinline))
void set(int val)
{
    ready.store(val, std::memory_order_relaxed);
}

__attribute__ ((noinline))
void print()
{
    int val = ready.load(std::memory_order_relaxed);
    std::cout << val << std::endl;
}
int main(int argc, char** argv)
{
    int data = std::stoi(argv[1]);
    set(data);
    print();
    return 0;
}

反彙編

set函數

410 00000000004013f0 <_Z3seti>:
411       = __m & __memory_order_mask;
412     __glibcxx_assert(__b != memory_order_acquire);
413     __glibcxx_assert(__b != memory_order_acq_rel);
414     __glibcxx_assert(__b != memory_order_consume);
415 
416     __atomic_store_n(&_M_i, __i, int(__m));
417   4013f0:   89 3d de 2d 00 00       mov    %edi,0x2dde(%rip)        # 4041d4 <ready>
418 }
419   4013f6:   c3                      retq
420   4013f7:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
421   4013fe:   00 00

關注set函數彙編的417行,atomic的store在intel架構下就是一個普通的mov指令。
print函數

423 0000000000401400 <_Z5printv>:
424 {
425   401400:   41 54                   push   %r12
426     std::cout << val << std::endl;
427   401402:   bf c0 40 40 00          mov    $0x4040c0,%edi
428 {
429   401407:   55                      push   %rbp
430   401408:   48 83 ec 08             sub    $0x8,%rsp
431     memory_order __b __attribute__ ((__unused__))
432       = __m & __memory_order_mask;
433     __glibcxx_assert(__b != memory_order_release);
434     __glibcxx_assert(__b != memory_order_acq_rel);
435 
436     return __atomic_load_n(&_M_i, int(__m));
437   40140c:   8b 35 c2 2d 00 00       mov    0x2dc2(%rip),%esi        # 4041d4 <ready>
438     std::cout << val << std::endl;
439   401412:   e8 09 fd ff ff          callq  401120 <_ZNSolsEi@plt>
440   401417:   48 89 c5                mov    %rax,%rbp

關注print函數彙編的437行,atomic的load在intel架構下也是一個普通的mov指令。

結論

在intel架構下,atomic原子變量的relaxed語義的讀寫操作與普通變量的開銷基本一致,沒有額外的硬件負擔。

memory_order_release、memory_order_acquire

程序示例:

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

struct alignas(64) data
{
    uint64_t shared_data{0};
    char pad[64 - sizeof(shared_data)]{};
    std::atomic<bool> a_ready{false};
    char pad1[64 - sizeof(a_ready)]{};
};

data* d_ptr{nullptr};

int main() {
    d_ptr = new data();
    std::thread writer([] {
        d_ptr->shared_data = 5;
        d_ptr->a_ready.store(true, std::memory_order_release);
    });

    std::thread reader([] {
        while (!d_ptr->a_ready.load(std::memory_order_acquire)) {}
        std::cout << "data is " << d_ptr->shared_data << std::endl;
    });

    writer.join();
    reader.join();
    delete d_ptr;
    return 0;
}

反彙編

388 0000000000401430 <_ZNSt6thread11_State_implINS_8_InvokerISt5tupleIJZ4mainEUlvE_EEEEE6_M_runEv>:
389         d_ptr->shared_data = 5;
390   401430:   48 8b 05 a1 2d 00 00    mov    0x2da1(%rip),%rax        # 4041d8 <d_ptr>
391   401437:   48 c7 00 05 00 00 00    movq   $0x5,(%rax)
392       = __m & __memory_order_mask;
393     __glibcxx_assert(__b != memory_order_acquire);
394     __glibcxx_assert(__b != memory_order_acq_rel);
395     __glibcxx_assert(__b != memory_order_consume);
396 
397     __atomic_store_n(&_M_i, __i, int(__m));
398   40143e:   c6 40 40 01             movb   $0x1,0x40(%rax)
399       { }
400 
401     void
402     _M_run() { _M_func(); }
403   401442:   c3                      retq   
404   401443:   90                      nop
405   401444:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
406   40144b:   00 00 00 00 
407   40144f:   90                      nop

寫線程(store(release))分析:

  1. 關注寫線程彙編的398行,atomic store的release語義在intel架構下就是普通的mov指令。沒有 mfence、sfence、lock 前綴等同步指令;
  2. 執行順序是:
    (1)先寫共享數據;
    (2)再寫 ready 標誌。
  3. 在 Intel 的 TSO 模型(Total Store Order) 下,硬件天然保證:
    (1)寫操作不會亂序(store-store 不重排),第二條 movb 不會在第一條之前可見;
    (2)因此,“release” 語義自動成立,即「在這次 store 之前的寫操作(shared_data = 5)」一定在這次 store(a_ready)之前對其他線程可見。
409 0000000000401450 <_ZNSt6thread11_State_implINS_8_InvokerISt5tupleIJZ4mainEUlvE0_EEEEE6_M_runEv>:
410   401450:   41 54                   push   %r12
411   401452:   55                      push   %rbp
412   401453:   48 83 ec 08             sub    $0x8,%rsp
413   401457:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
414   40145e:   00 00 
415     memory_order __b __attribute__ ((__unused__))
416       = __m & __memory_order_mask;
417     __glibcxx_assert(__b != memory_order_release);
418     __glibcxx_assert(__b != memory_order_acq_rel);
419 
420     return __atomic_load_n(&_M_i, int(__m));
421   401460:   48 8b 05 71 2d 00 00    mov    0x2d71(%rip),%rax        # 4041d8 <d_ptr>
422   401467:   0f b6 40 40             movzbl 0x40(%rax),%eax
423         while (!d_ptr->a_ready.load(std::memory_order_acquire)) {}
424   40146b:   84 c0                   test   %al,%al
425   40146d:   74 f1                   je     401460 <_ZNSt6thread11_State_implINS_8_InvokerISt5tupleIJZ4mainEUlvE0_EEEEE6_M_runEv+0x10>

讀線程(load(acquire))分析:

  1. 讀線程彙編的422行,movzbl 0x40(%rax), %eax 是普通的 load;
  2. 沒有任何 fence 或 lock;
  3. 語義上的“acquire”保證在於:
    (1)編譯器不會把這次 load(對 a_ready)重排到後面的讀(shared_data)之後;
    (2)而在 Intel 的硬件層:雖然 TSO 允許 store-load 亂序,但不會發生 load-load 亂序。也就是説,本線程後續的讀操作不會被重排到這次 acquire 之前,而跨線程的可見性仍由緩存一致性協議保證。

    結論

    在intel架構下,atomic原子變量的release/acquire語義的讀寫操作與普通變量的開銷十分接近,只是對編譯器亂序進行了限制,幾乎沒有額外的硬件負擔。

    memory_order_seq_cst

    程序示例:

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

struct alignas(64) SharedData {
    std::atomic<int> a{0};
    std::atomic<int> b{0};
};

SharedData data;

void thread1() {
    data.a.store(1, std::memory_order_seq_cst); // write a
    int b_val = data.b.load(std::memory_order_seq_cst); // read b
    std::cout << "Thread1 read b = " << b_val << std::endl;
}

void thread2() {
    data.b.store(2, std::memory_order_seq_cst); // write b
    int a_val = data.a.load(std::memory_order_seq_cst); // read a
    std::cout << "Thread2 read a = " << a_val << std::endl;
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    std::cout << "Final values: a = " << data.a.load() 
              << ", b = " << data.b.load() << std::endl;
}

反彙編

349 00000000004013a0 <_Z7thread2v>:
350 void thread2() {
351   4013a0:   41 54                   push   %r12
352     __atomic_store_n(&_M_i, __i, int(__m));
353   4013a2:   b8 02 00 00 00          mov    $0x2,%eax
354   4013a7:   55                      push   %rbp
355   4013a8:   48 83 ec 08             sub    $0x8,%rsp
356   4013ac:   87 05 52 2e 00 00       xchg   %eax,0x2e52(%rip)        # 404204 <data+0x4>
357     __ostream_insert(__out, __s,
358   4013b2:   ba 11 00 00 00          mov    $0x11,%edx
359   4013b7:   be 10 20 40 00          mov    $0x402010,%esi
360   4013bc:   bf c0 40 40 00          mov    $0x4040c0,%edi
361     return __atomic_load_n(&_M_i, int(__m));
362   4013c1:   8b 2d 39 2e 00 00       mov    0x2e39(%rip),%ebp        # 404200 <data>
363   4013c7:   e8 f4 fc ff ff          callq  4010c0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
364     std::cout << "Thread2 read a = " << a_val << std::endl;
365   4013cc:   89 ee                   mov    %ebp,%esi
366   4013ce:   bf c0 40 40 00          mov    $0x4040c0,%edi
367   4013d3:   e8 48 fd ff ff          callq  401120 <_ZNSolsEi@plt>
368   4013d8:   48 89 c5                mov    %rax,%rbp
411 0000000000401450 <_Z7thread1v>:
412 void thread1() {
413   401450:   41 54                   push   %r12
414     __atomic_store_n(&_M_i, __i, int(__m));
415   401452:   b8 01 00 00 00          mov    $0x1,%eax
416   401457:   55                      push   %rbp
417   401458:   48 83 ec 08             sub    $0x8,%rsp
418   40145c:   87 05 9e 2d 00 00       xchg   %eax,0x2d9e(%rip)        # 404200 <data>
419     __ostream_insert(__out, __s,
420   401462:   ba 11 00 00 00          mov    $0x11,%edx
421   401467:   be 22 20 40 00          mov    $0x402022,%esi
422   40146c:   bf c0 40 40 00          mov    $0x4040c0,%edi
423     return __atomic_load_n(&_M_i, int(__m));
424   401471:   8b 2d 8d 2d 00 00       mov    0x2d8d(%rip),%ebp        # 404204 <data+0x4>
425   401477:   e8 44 fc ff ff          callq  4010c0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
426     std::cout << "Thread1 read b = " << b_val << std::endl;
427   40147c:   89 ee                   mov    %ebp,%esi
428   40147e:   bf c0 40 40 00          mov    $0x4040c0,%edi
429   401483:   e8 98 fc ff ff          callq  401120 <_ZNSolsEi@plt>
430   401488:   48 89 c5                mov    %rax,%rbp

分析
從反彙編可以看到,std::atomic 使用 memory_order_seq_cst 時,編譯器在 store 指令處生成了帶 lock 前綴的原子操作(xchg指令,彙編356、418行),而在 load 指令處仍然是普通的 mov(363、424行)。
這是因為在 Intel 的 TSO(Total Store Order)內存模型 下,處理器天然保證:

  • Store → Store 不亂序
  • Load → Load 不亂序
  • Load → Store 不亂序

唯一可能亂序的是 Store → Load,即寫入尚滯留在本核的 store buffer 時,後續的讀取可以越過尚未刷新出去的寫操作執行。這種亂序不會影響單線程一致性,但會導致多線程間可見性問題。
為防止這種 “寫後讀亂序”,seq_cst 模型要求全序內存語義,因此編譯器通過插入 lock 前綴的原子指令(如 xchg)隱式加入了一個 full fence:

  • 它會強制刷新本核的 store buffer;
  • 並阻止後續的讀操作提前執行。

這樣就保證了 當前線程的所有寫在全局上先於後續的所有讀、寫可見,實現了符合 C++ memory_order_seq_cst 要求的全序一致性。
而在讀取側,由於 TSO 模型天然禁止 Load→Load 與 Load→Store 亂序,因此 load 操作無需額外的 fence,只要保證讀取時能看到其他核心已經刷新出去的寫即可。
綜上,x86 架構上 seq_cst 的實現核心在於:

  • 僅在 store 側 插入全柵欄(通常通過 LOCK 指令隱式實現,也可能直接使用 mfence),以防止 Store→Load 亂序;其餘的順序約束則由 TSO 硬件自然保證。

結論

在 Intel 架構下,atomic 的 seq_cst 寫操作會插入全柵欄(可能是隱式的lock 指令方式),存在一定硬件開銷;而讀操作與普通變量的開銷十分接近,幾乎沒有額外的硬件負擔,只是對編譯器重排做了限制。

四、性能角度分析

在多線程程序中,std::atomic 提供的內存序(Memory Order)不僅決定了數據在不同線程間的可見性,也直接影響指令執行的性能與編譯器優化空間。以下從性能角度分析INTEL架構下各個內存序對當前線程的影響和可見性保障程度。

本線程影響

內存序 對當前線程性能影響 是否對編譯器重排有限制 跨線程可見性保障 是否引入硬件屏障
relaxed 幾乎無性能損耗,可完全被編譯器優化 否:不禁止編譯器重排 不保證順序,僅保證原子性
acquire 讀操作輕量 是:禁止後續讀操作越過該 load 保證看到對應 release 寫之前的所有寫入 否(x86 TSO 保證)
release 寫操作輕量 是:禁止前面寫操作延後越過該 store 保證當前線程寫在被 acquire 讀到前全部可見 否(x86 TSO 保證)
acq_rel 讀寫組合 是:禁止前寫後寫 & 後讀前讀重排 雙向保證可見性 否(x86 TSO 保證)
seq_cst 最重,會強制全序語義 是:禁止所有讀寫跨越 全局單一順序,最強可見性 是(x86 會生成 lock 指令)

可見性影響

內存序 是否立即刷新 Store Buffer 對其他線程可見性 對當前線程開銷 説明
relaxed 可能延遲看到舊值 極低(和普通變量差不多) 僅保證當前線程順序,不保證跨線程同步
release 寫可能延遲其他線程可見 保證寫在 release 之前的操作順序可見,但寫本身仍在 store buffer
acquire 讀可能讀到舊值 保證 acquire 之後的讀不被重排,但不強制刷新 store buffer
acq_rel 寫和讀都可能延遲可見 寫和讀結合 release/acquire 的特性
seq_cst 寫立即對其他線程可見 較高(插入 fence 或 lock 指令) 保證全局一致順序,硬件強制刷新 store buffer,延遲略高

説明

  • Intel TSO 保證當前線程內的寫-讀順序不亂序,但跨線程可見性依賴 store buffer 刷新。
  • seq_cst 是為了在多線程間實現嚴格全序,確保寫立即可見。
  • release/acquire 在 Intel 下,由於 TSO,本質上只需編譯器屏障即可,無額外硬件操作。

五、總結

通過對 Intel 架構下 C++ 內存模型和原子操作彙編的分析,我們可以得出幾個重要結論:

  1. 理解硬件機制有助優化性能
    TSO 提供天然順序保證,store buffer 決定跨線程可見性。
  2. 內存序選擇影響開銷與可見性
    relaxed 開銷小但可見性延遲,release/acquire 限制編譯器重排,seq_cst 寫操作引入屏障保證全局順序。
  3. 優化關鍵
    合理選擇內存序、注意緩存行對齊和 false sharing,可顯著降低延遲。

📬 歡迎關注公眾號“Hankin-Liu的技術研究室”,收徒傳道。持續分享信創、軟件性能測試、調優、編程技巧、軟件調試技巧相關內容,輸出有價值、有沉澱的技術乾貨。

user avatar sherlocked93 头像 hlinleanring 头像 Yzi321 头像 hedzr 头像
点赞 4 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.