大家應該或多或少在engine中都會看到volatile這個關鍵字,通常這個詞都是跟某個系統上的cache機制有緊密的關係,被宣告成volatile,代表的是他是隨時會被各種不受控制的外部系統改變,因此系統不會對該資料進行cache的動作。
在C++中,對某個變數i宣告volatile這個關鍵字又代表著什麼意思呢?對cpu而言,他對於變數的cache機制不難想像,就是他身上的暫存器 (Register)。
基本上不管編譯時期的compiler或運行時期的cpu,他們會盡一切手段去優化執行的效率,例如把變數變成常數,reorder指令的順序或者是把某個值i從記憶體中讀進cpu的register中,然後接下來的指令就直接從該register位置中拿值而不再去從記憶體中要。雖然這些優化手段在使用上都會是保守的手段,理論上會被保存在register中的數值,對於接下來運行的指令而言應該是可信的,例如:
int i=10;
int a = i;
int b = i;
i=10在這三條指令的範圍內應該要是是可信的。
但還是有些例外,例如該值i是被其他外部程序直接更改記憶體中的值的時候呢(*1)?比如a先assign了10之後,i指馬上被外部程式改成了11,這時候由於cpu register沒機會去刷新,因此b還會維持10的結果,但理論上應該要是11才對。這時候volatile這個關鍵字就派上用場了,前面也提到,被宣告成volatile,我們的系統就不會去cache他,因此我們每次都重新去從記憶體中拿值而不是從register中。
*1:這邊通常是硬體搭配Memory mapping I/O使用。利用系統保留的記憶體區間對應一個裝置上的幾個register數值,在程式中操作記憶體等同於操作該裝置。
不過在multithreading的使用情境中,volatile並不等同於thread-safe。雖然compile time時的優化被volatile指令阻止了,包括禁止cache跟reorder指令。然而,雖然你的指令順序有乖乖照你希望的被編譯出來,但cpu可沒那麼聽話,他run time的reorder還是會照常運行。
因此要thread-safe我們還是要搭配有同步(synchronization)機制的正規手段才行,例如atomic、mutex或memory fence,換句話說,在multi-thread的環境下,指令的執行順序很重要,因此上面這幾個工具就是設計來確保程式能夠叫compiler跟cpu都能夠乖乖照我的邏輯執行,不要亂做奇怪的優化並籍此防止race condition。
另外volatile的實際行為,要不要reorder以及怎麼reorder也硬體自身的實作有關,因此太依賴這個東西的話可能會在跨平台的應用上踩到某些雷。
不過在unreal的搜原始碼中,我們會看到有一些multi-thread用的變數除了有用Atomic相關工具之外,還是被加上了volatile,就我的認知中,他最好只在使用MMIO跟硬體存取搭配時使用。關於這點我思考了很久,會不會是因為歷史因素照成的?或是某些平台,單純使用上面提到的幾個同步工具時不可靠?這點我還沒有答案,也有可能是我的認知還不夠完善,有人知道原因的話也歡迎提出來討論。
Leave a Reply