2015-10-17 74 views
2

我們都知道這樣`volatile`是否允許與工會打字?

union U {float a; int b;}; 

U u; 
std::memset(u, 0, sizeof u); 
u.a = 1.0f; 
std::cout << u.b; 

這種類型的雙關在C未定義行爲++。

它是未定義的,因爲在u.a = 1.0f;賦值之後.a變爲活動字段並且.b變爲非活動字段,並且它是未定義行爲以從非活動字段中讀取。我們都知道這一點。


現在,請考慮下面的代碼

union U {float a; int b;}; 

U u; 
std::memset(u, 0, sizeof u); 
u.a = 1.0f; 

char *ptr = new char[std::max(sizeof (int),sizeof (float))]; 
std::memcpy(ptr, &u.a, sizeof (float)); 
std::memcpy(&u.b, ptr, sizeof (int)); 

std::cout << u.b; 

而現在它成爲明確的,因爲這種類型的雙關語是允許的。 另外,如您所見,調用後內存保持不變,如u


現在我們來添加線索和 volatile關鍵字。

union U {float a; int b;}; 

volatile U u; 
std::memset(u, 0, sizeof u); 
u.a = 1.0f; 

std::thread th([&] 
{ 
    char *ptr = new char[sizeof u]; 
    std::memcpy(ptr, &u.a, sizeof u); 
    std::memcpy(&u.b, ptr, sizeof u); 
}); 
th.join(); 

std::cout << u.b; 

邏輯保持不變,但我們只有第二個線程。由於volatile關鍵字代碼保持良好定義。

在真正的代碼中,第二個線程可以通過任何糟糕的線程庫實現,編譯器可以不知道第二個線程。但由於volatile關鍵字,它仍然是明確的。


但是如果沒有其他線程呢?

union U {float a; int b;}; 

volatile U u; 
std::memset(u, 0, sizeof u); 
u.a = 1.0f; 
std::cout << u.b; 

沒有其他線程。 但編譯器不知道沒有其他線程!

從編譯器的角度來看,沒有什麼改變!如果第三個例子是明確的,那麼最後一個例子也必須是明確的!

而且我們不需要第二個線程,因爲它不會改變u內存。


如果使用 volatile,則編譯器假定 u可以在任何時候以靜默方式進行修改。在這種修改下,任何領域都可以變得活躍

因此,編譯器無法跟蹤易失性聯合的哪個字段處於活動狀態。 即使沒有真正修改該聯合,它也不能假定某個字段在分配給它(並且其他字段保持不活動狀態)後仍保持活動狀態。

所以,在最後兩個例子中編譯器會給我精確的位表示1.0f轉換爲int


問題是: 我的推理是否正確?第三和第四個例子真的很好嗎?標準對此有何評論?

+0

'volatile'與線程無關。請參閱[爲什麼存在volatile?](http://stackoverflow.com/questions/72552/why-does-volatile-exist?rq=1) –

+0

首先,在第四個示例中,您讀取一個沒有同步的變量,因此編譯器可以假設沒有其他線程寫入它。 'volatile'只意味着對變量的每一個操作都是可觀察的副作用;這與多線程執行無關。所以這個例子肯定是錯誤的。第二:你爲什麼首先做這樣的事情? –

+0

@BaummitAugen 1.我做這樣的事情,因爲它看起來像在C++中簡單類型雙擊的黑客攻擊。 2.你的意思是,當'compiler'看到'th.join()'時,它認爲voltaile變量有可能被修改了嗎?但是如果我使用任何非標準的線程庫,例如SDL線程呢?編譯器不知道'SDL_WaitThread()'加入了一個線程。 3.「volatile只意味着對變量的每一個操作都是可觀察的副作用」你能解釋一下嗎?我不明白你的意思。 – HolyBlackCat

回答

9

在實際代碼中,第二個線程可以通過任何蹩腳的線程庫實現,編譯器可以不知道第二個線程。但由於易變的關​​鍵字,它仍然是明確的。

該陳述是錯誤的,所以您基於您的結論的其他邏輯是不健全的。

假設你有這樣的代碼:

int* currentBuf = bufferStart; 
while(currentBuf < bufferEnd) 
{ 
    *currentBuf = foobar;  
    currentBuf++; 
} 

如果foobar是不揮發那麼編譯器被允許的理由如下:「我知道,foobar的永遠不會被currentBuf鋸齒,因此內不會改變環,因此,我可以優化碼爲」

int* currentBuf = bufferStart; 
int temp = foobar; 
while(currentBuf < bufferEnd) 
{ 
    *currentBuf = temp;  
    currentBuf++; 
} 

如果foobarvolatile然後本產品和許多其它代碼生成優化是禁用。注意我說代碼生成。該CPU是完全有權利但移動讀取和寫入圍繞其心臟的內容,前提是CPU的內存模型不受侵犯。

特別是,編譯器不需要強制CPU返回到主內存每次讀與寫foobar全部它需要做的是避免某些優化。 (這不是嚴格符合的;編譯器也有義務確保涉及長跳轉的特定屬性被保留,以及一些與線程無關的其他小細節。)如果有兩個線程,並且每個線程都不同處理器,並且每個處理器具有不同的高速緩存,volatile不會要求將高速緩存設置爲一致,前提是它們都包含foobar的內存副本。

一些編譯器可以選擇實施爲您提供方便的語義,但他們這樣做不是必需的;請查閱你的編譯器文檔

我注意到C#和Java 需要獲取和釋放揮發物的語義,但這些要求可能會令人驚訝地薄弱。尤其是,x86不會對兩個易失性寫入或兩個易失性讀取進行重新排序,但在另一個易失性寫入之前允許對一個變量的易失性重新排序,事實上x86處理器在極少數情況下可以這樣做。 (對於C#編寫的一個難題,說明低鎖碼可怎麼是錯誤的見http://blog.coverity.com/2014/03/26/reordering-optimizations/即使一切都揮發,具有獲得釋放語義。)

寓意是:即使你的編譯器是有益的,並施加額外像C#或Java volatile變量語義,它可能仍然是不存在持續觀察到序列的讀取和在所有線程寫入的情況下;許多內存模型並沒有強加這個要求。這會導致奇怪的運行時行爲。同樣,如果你想知道什麼volatile意味着您諮詢您的編譯器文檔

1

不 - 您的推理是錯誤的。易變的部分是一個普遍的誤解 - 易失性是不工作的,因爲你說。

聯合部分是錯誤的。閱讀本Accessing inactive union member and undefined behavior?

用C++(11)你只能指望正確的/良好定義的行爲,當最後的寫入對應的再現。

相關問題