龍芯 LA664 核心無屏障原子指令會遺失更新
jia.je(賈杰)部落格 · 2026-09-24
龍芯(Loongson)LA664 核心在特定條件下,amadd.d、amcas.d、amswap.d 等不帶資料屏障的原子指令會偶爾「不原子」——多執行緒同時遞增同一個計數器時,部分更新會憑空消失。這台裝著 3A6000/3C6000 系列處理器的機器,違反了 LoongArch 規格對原子操作「read-modify-write 不可被打斷」的保證。舊款 LA464 核心(如 3A5000)不受影響,問題只出在新一代的 LA664。
原本的問題
作者在替 normaliz(一套數論函式庫)除錯時,發現一段用 OpenMP atomic 遞增的計數器會陷入無窮迴圈:跑到最後,計數器的實際值比理論上該遞增的次數還要少。一開始懷疑是編譯器或函式庫邏輯錯誤,但把程式簡化到只剩「多執行緒對同一位址做原子加一」後,問題卻消失了——這代表原子加法指令本身沒壞,壞的是某種特定情境下的交互作用。
LoongArch 規格明訂 am<op>.* 系列指令必須完成不可分割的讀—改—寫,多核心同時對同一位址操作時不能互相覆蓋。這是無鎖資料結構(lock-free)、參照計數(如 Rust 的 Arc、mpsc::Sender)等機制賴以正確運作的底層假設,一旦硬體本身不遵守,上層再嚴謹的演算法也救不回來。
如何發現與重現
作者靠 AI 輔助縮小範圍,把矛頭指向 glibc 的向量化 memcpy:只要另一條執行緒在做 LASX 向量化的記憶體搬移,原子計數器才會漏加。兩天內做出最小重現案例,確認觸發需要同時滿足三個條件:
- 兩條執行緒跑在不同的實體核心上(而非同一核心的 SMT 分身)
- 對同一位址使用不帶資料屏障的原子操作(如
amadd.d) - 其中一條執行緒在原子操作之間穿插了記憶體讀取——尤其是
xvld這類 LASX 向量讀取
在 3C6000/S 的兩顆實體核心上跑 30 次試驗,兩邊都用 LASX 讀取時失敗率接近 100%;amadd.d 搭配 LASX 複製約 67% 會漏加。反觀帶資料屏障的 amcas_db.d,同樣條件下失敗率是 0%——這正是問題出在「無屏障」變體本身,而不是原子性設計理念的證據。
| 指令類型 | LoongArch 規格預期語意 | LA664 實測行為 |
|---|---|---|
amadd.d / amcas.d / amswap.d(無屏障) | read-modify-write 不可分割,跨核心互斥 | 與同址 LASX 讀取交錯時,更新可能遺失(最高近 100% 失敗率) |
amcas_db.d 等帶 _db 屏障變體 | 同上,且附加記憶體屏障 | 相同條件下 30 次試驗 0 次失敗,未觀察到問題 |
影響範圍與因應
受影響的是任何跑在龍芯 LA664 平台、又依賴不帶屏障原子指令的軟體:glibc 內部用 memcpy 加速的程式、Rust 生態中大量使用 Arc/mpsc 做參照計數的服務、以及一切手寫無鎖計數器或佇列的 C/C++ 專案,都可能在龍芯新機器上悄悄漏算,且現象只在多核心+向量讀取同時發生時才出現,很難用一般壓力測試抓到。
作者列出的因應方式分成韌體與軟體兩層。韌體層可將 MCSR24 這顆控制暫存器的第 13 位元設為 1,性能損失很小。軟體層則有幾條路:改用帶 _db 屏障的原子指令變體、退回 LL/SC 迴圈實作、以環境變數 GLIBC_TUNABLES=glibc.cpu.hwcaps=-LASX 停用 LASX,或是重新編譯 glibc 時加上 --disable-multi-arch 關掉向量化多架構分派。對正在龍芯平台上部署高並發服務的團隊而言,最務實的第一步是先確認韌體是否已修正 MCSR24,再檢查自身程式碼裡有沒有裸用無屏障原子指令的位置。