[英]Should std::atomic be volatile?
我正在運行一個運行的線程,直到設置了一個標志。
std::atomic<bool> stop(false);
void f() {
while(!stop.load(std::memory_order_{relaxed,acquire})) {
do_the_job();
}
}
我想知道編譯器是否可以像這樣展開循環(我不希望它發生)。
void f() {
while(!stop.load(std::memory_order_{relaxed,acquire})) {
do_the_job();
do_the_job();
do_the_job();
do_the_job();
... // unroll as many as the compiler wants
}
}
據說波動率和原子性是正交的,但我有點困惑。 編譯器是否可以自由緩存原子變量的值並展開循環? 如果編譯器可以展開循環,那么我認為我必須將volatile
放到標志中,我想確定。
我應該把volatile
?
我很抱歉曖昧。 我(我猜我)理解重新排序是什么以及memory_order_*
是什么意思,我確信我完全理解volatile
是什么。
我認為while()
循環可以轉換為這樣的無限if
語句。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
if(stop.load(std::memory_order_{relaxed,acquire})) return;
do_the_job();
...
}
由於給定的內存順序不會阻止序列化之前的操作被移動超過原子載荷,我認為如果它沒有易失性,它可以重新排列。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
if(stop.load(std::memory_order_{relaxed,acquire})) return;
if(stop.load(std::memory_order_{relaxed,acquire})) return;
...
do_the_job();
do_the_job();
do_the_job();
...
}
如果原子並不意味着易失性,那么我認為在最壞的情況下代碼甚至可以像這樣轉換。
void f() {
if(stop.load(std::memory_order_{relaxed,acquire})) return;
while(true) {
do_the_job();
}
}
永遠不會有這樣瘋狂的實施,但我想這仍然是一個可能的情況。 我認為防止這種情況的唯一方法是將volatile
放到原子變量上並詢問它。
我做了很多猜測,請告訴我他們之間是否有什么問題。
編譯器是否可以自由緩存原子變量的值並展開循環?
編譯器無法緩存原子變量的值。
但是,由於您使用的是std::memory_order_relaxed
,這意味着編譯器可以自由地對此原子變量的加載和存儲進行重新排序,以及其他加載和存儲。
另請注意,對此轉換單元中定義不可用的函數的調用是編譯器內存屏障。 這意味着調用不能對周圍的加載和存儲進行重新排序,並且所有非局部變量必須在調用后從內存中重新加載,就好像它們都標記為volatile一樣。 (不會重新加載其地址未在其他地方傳遞的局部變量)。
您希望避免的代碼轉換不是有效的轉換,因為這會違反C ++內存模型:在第一種情況下,您有一個原子變量的加載,然后調用do_the_job
,在第二種情況下,您有多個調用。 觀察到的變換代碼的行為可能不同。
與揮發性的關系
在執行的一個線程中,對所有易失性對象的訪問(讀取和寫入)保證不會相對於彼此重新排序,但是這個順序不能保證被另一個線程觀察到,因為易失性訪問不會建立線程間同步。
此外,易失性訪問不是原子的(並發讀和寫是數據競爭)並且不命令存儲器( 非易失性存儲器訪問可以在易失性訪問周圍自由重新排序 )。
這種位非易失性存儲器訪問可以在易失性訪問周圍自由地重新排序,對於輕松原子也是如此,因為可以關於其他加載和存儲重新排序松弛的加載和存儲。
換句話說,用volatile
來裝飾你的原子不會改變代碼的行為。
無論如何,C ++ 11原子變量不需要用volatile
關鍵字標記。
以下是g ++ - 5.2如何表達原子變量的示例。 以下功能:
__attribute__((noinline)) int f(std::atomic<int>& a) {
return a.load(std::memory_order_relaxed);
}
__attribute__((noinline)) int g(std::atomic<int>& a) {
static_cast<void>(a.load(std::memory_order_relaxed));
static_cast<void>(a.load(std::memory_order_relaxed));
static_cast<void>(a.load(std::memory_order_relaxed));
return a.load(std::memory_order_relaxed);
}
__attribute__((noinline)) int h(std::atomic<int>& a) {
while(a.load(std::memory_order_relaxed))
;
return 0;
}
用g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S
編譯g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S
g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S
生成以下程序集:
f(std::atomic<int>&):
movl (%rdi), %eax
ret
g(std::atomic<int>&):
movl (%rdi), %eax
movl (%rdi), %eax
movl (%rdi), %eax
movl (%rdi), %eax
ret
h(std::atomic<int>&):
.L4:
movl (%rdi), %eax
testl %eax, %eax
jne .L4
ret
如果do_the_job()
沒有改變stop
,那么編譯器是否可以展開循環並不重要。
std::memory_order_relaxed
只是確保每個操作都是原子操作,但它不會阻止重新排序訪問。 這意味着如果另一個線程將stop
設置為true
,則循環可以繼續執行幾次,因為可以重新排序訪問。 所以它與展開循環的情況相同: do_the_job()
可能在另一個線程將stop
設置為true
后執行幾次。
所以不,不要使用volatile
,使用std::memory_order_acquire
和std::memory_order_release
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.