記憶體模型與原子操作

編譯器重排相對於 CPU 重排

你的原始碼只是對事情該以什麼順序發生的一個禮貌建議,而非對它們「將會」以什麼順序發生的具有約束力的契約。有兩個完全獨立的當事方會擅自更動那個順序,而要寫出正確的並行程式,你必須「同時」約束兩者。只約束其中一個,另一個就能自由地把你弄壞。

第一個當事方是「編譯器」。在編譯期,最佳化器自由地搬動載入與儲存、把值留在暫存器裡而不再重讀記憶體、合併或刪除多餘的存取、把不變的讀取上提出迴圈——這一切都合法,因為對「單一」執行緒而言可見結果不變(as-if 規則)。第二個當事方是「中央處理器」。在執行期,即使編譯出的指令已固定,硬體仍會重排記憶體操作:它有延遲寫入的儲存緩衝區、它亂序執行、它可能讓較晚的載入比較早的儲存先完成。在 x86 上這種 CPU 重排受限(TSO);在 ARM 與 POWER 上則自由得多。

要擋住這兩者需要不同的肌肉。一道編譯器屏障(例如 asm volatile("" ::: "memory"),或更正規地、一個帶排序標籤的原子)擋住「編譯器」重排,但不發出任何 CPU 指令。一道硬體屏障(mfence、dmb、lwsync)擋住「中央處理器」,但編譯器也必須被告知。C11/C++ 原子的妙處在於:一個 memory_order 標籤同時約束兩者——它告訴最佳化器哪些不可重排,「並」讓它發出目標平台所需的那道 CPU 屏障。這正是為何你該伸手拿原子,而非自製的內嵌組語屏障——也是為何「在 x86 上不用原子也能跑」是個陷阱:即使 x86 的中央處理器不會重排,編譯器仍可能重排。

/* 沒有原子時,編譯器可能把這個... */ while (done == 0) { } /* ...靠把載入上提一次,變成這個: */ if (done == 0) for (;;) { } /* 即使另一個執行緒之後設了 done=1,仍是無限迴圈 */

純粹的編譯器重排,與中央處理器無關:最佳化器把 done 快取進暫存器,再也不重讀它。

只約束中央處理器(光一道硬體屏障)或只約束編譯器,是經典的半套修法;用語言層的原子,好讓「一個」排序標籤同時釘住兩者——並記得即使在中央處理器幾乎不重排的 x86 上,編譯器仍有完全的重排許可。

又稱
instruction reordering重排序