工程趣聞 2026 年 9 月 27 日

2026-09-27 — 龍芯 LA664 核心無屏障原子指令會遺失更新

primary=https://jia.je/hardware/2026/09/24/loongson-cpu-erratum-en/

龍芯 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,再檢查自身程式碼裡有沒有裸用無屏障原子指令的位置。

原始來源:The lost atomic update on LoongArch LA664


End of article
0
Would love your thoughts, please comment.x
()
x