連結載入/條件儲存(load-linked / store-conditional, LL/SC)
/ EL-EL-ES-SEE /
比較並交換問一個問題——這個值還是我預期的嗎?——但它靠比較位元來回答,若值變掉又變回去就會被騙。連結載入/條件儲存(LL/SC)是另一種硬體原語,用於 ARM、RISC-V、POWER 與 MIPS,它改問一個更銳利的問題:自我上次查看以來,這個位置到底有沒有「被寫過」?它由一對協同運作的指令構成。
舞步如下。連結載入(LL)讀取一個記憶體位置,並悄悄告訴硬體開始盯著那個位址——它設下一個隱藏的保留(reservation)。接著你在一般暫存器裡算出新值。最後,條件儲存(SC)嘗試寫回,但唯有自你的 LL 以來那個位址沒被寫過,它才會成功;否則它什麼都不寫並回報失敗。與 CAS 最關鍵的差別在於:只要中間有「任何」寫入,SC 就失敗,即使那是一次把值寫回完全相同數值的寫入。這就是 LL/SC 能避開困擾樸素 CAS 的 ABA 問題的原因:它盯的是寫入這個事件本身,而非最終值的改變。如同弱版 CAS,你會把它包進重試迴圈。
誠實的麻煩是:保留很脆弱。SC 容許因與你的資料無關的原因而假性失敗——一次上下文切換、一個中斷、對同一條快取列(cache line)的另一次存取,或僅僅是在 LL 與 SC 之間碰了記憶體。所以可攜的程式碼會把 LL 到 SC 的窗口縮到極小:幾個暫存器運算,不呼叫函式,不做其他載入或儲存。正因為這份脆弱,較高階的語言對外暴露的是 CAS 而非 LL/SC,編譯器則在擁有 LL/SC 的機器上以它為基礎合成出比較並交換。
; AArch64 用 LL/SC 構成的原子遞增,重試到 STXR 成功為止。 loop: ldxr x1, [x0] ; 連結載入:讀取並對 [x0] 設下保留 add x1, x1, #1 ; 算出新值 stxr w2, x1, [x0] ; 條件儲存:成功則 w2 = 0 cbnz w2, loop ; 若 w2 != 0 表示 SC 失敗,重試
唯有自 LDXR 以來沒有任何東西寫過該位址,STXR 才成功——包括把值寫回相同數值的寫入,這就是 LL/SC 天生免疫 ABA 的原因。
SC 可能因與你的值無關的原因(中斷、快取列流量)假性失敗,所以要把 LL 到 SC 的窗口縮到極小並總是迴圈重試。LL/SC 靠盯任何寫入、而非盯值的改變來避開 ABA。