簡體   English   中英

允許編譯器優化掉局部volatile變量嗎?

[英]Is it allowed for a compiler to optimize away a local volatile variable?

是否允許編譯器對此進行優化(根據C ++ 17標准):

int fn() {
    volatile int x = 0;
    return x;
}

對此嗎?

int fn() {
    return 0;
}

如果是,為什么? 如果沒有,為什么不呢?


關於此主題的一些思考:當前的編譯器將fn()編譯為放置在堆棧中的局部變量,然后將其返回。 例如,在x86-64上,gcc創建以下代碼:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

現在,據我所知,標准並沒有說應該將局部volatile變量放入堆棧中。 因此,此版本同樣不錯:

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

edx在這里存儲x 但是現在,為什么在這里停下來? 由於edxeax均為零,我們可以這樣說:

xor    eax,eax // eax is the return, and x as well
ret    

然后我們將fn()轉換為優化版本。 此轉換有效嗎? 如果不是,哪一步無效?

不能。對volatile對象的訪問被視為可觀察到的行為,與I / O完全一樣,在本地變量和全局變量之間沒有特別的區別。

符合標准的實現的最低要求是:

  • 嚴格根據抽象機的規則評估對volatile對象的訪問。

[...]

這些統稱為程序的可觀察行為。

N3690,[介紹執行],¶8

究竟如何 ,這是觀察到的是標准的范圍之內,直落入實現特定的領土,正是因為I / O,並獲得全球volatile物。 volatile意味着“您認為您知道這里發生的一切,但事實並非如此;請相信我,並且不要太聰明,因為我在您的程序中使用字節來做我的秘密工作”。 這實際上在[dcl.type.cv]¶7中有解釋:

[注意: volatile是實現的一種避免使用對象的優化的提示,因為對象的值可能通過實現無法檢測的方式進行更改。 此外,對於某些實現,volatile可能指示需要特殊的硬件指令才能訪問該對象。 有關詳細的語義,請參見1.9。 通常,在C ++中,volatile的語義應與在C中相同。

該循環可以通過按規則進行優化,因為它沒有可觀察到的行為:

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

此人不能:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

第二個循環在每次迭代中執行某些操作,這意味着該循環需要O(n)時間。 我不知道常量是什么,但是我可以測量它,然后有一種忙循環(或多或少)的已知時間的方法。

我之所以能夠這樣做,是因為該標准規定必須依次訪問揮發物。 如果編譯器決定在這種情況下該標准不適用,我想我將有權提交錯誤報告。

如果編譯器選擇將looped放入寄存器,我想我對此沒有很好的論據。 但是它仍然必須為每次循環迭代將該寄存器的值設置為1。

盡管完全了解volatile意味着可觀察的I / O,但我還是反對大多數意見。

如果您有此代碼:

{
    volatile int x;
    x = 0;
}

我相信編譯器可以根據假設 規則優化它, 前提是:

  1. volatile變量,否則不通過外部如指針可見(這顯然不是一個問題就在這里,因為沒有這樣的事情在給定范圍內)

  2. 編譯器沒有為您提供從外部訪問該volatile的機制

理由很簡單,由於標准2,您仍然無法觀察到差異。

但是,在您的編譯器中, 可能無法滿足條件#2 編譯器可能會嘗試為您提供有關從“外部”觀察volatile變量的額外保證,例如通過分析堆棧。 在這種情況下,行為實際上可觀察的,因此無法對其進行優化。

現在的問題是,以下代碼與上面的代碼有什么不同嗎?

{
    volatile int x = 0;
}

我相信我在Visual C ++中觀察到了關於優化的不同行為,但是我不確定是基於什么理由。 可能初始化不算作“訪問”? 我不確定。 如果您有興趣,這可能是一個值得單獨回答的問題,但是否則,我相信答案如上所述。

從理論上講,中斷處理程序可以

  • 檢查返回地址是否在fn()函數內。 它可能通過檢測或附加的調試信息訪問符號表或源代碼行號。
  • 然后更改x的值,該值將以距堆棧指針可預測的偏移量存儲。

…因此使fn()返回一個非零值。

我將為as-if規則和volatile關鍵字添加詳細的參考。 (在這些頁面的底部,遵循“另請參見”和“參考”以追溯到原始規格,但我發現cppreference.com更易於閱讀/理解。)

特別是,我希望您閱讀本節

volatile對象-類型為volatile限定的對象,或volatile對象的子對象,或const-volatile對象的可變子對象。 出於優化目的(即在單個執行線程中,通過volatile限定類型的glvalue表達式進行的每次訪問(讀或寫操作,成員函數調用等)都被視為可見的副作用)無法優化訪問,也不會在volatile訪問之前或之后的另一個可見副作用進行優化或重新排序,這使得volatile對象適合與信號處理程序進行通信,但不適合與其他執行線程進行通信,請參見std :: memory_order )。 任何通過非易失性glvalue來引用易失性對象的嘗試(例如,通過對非易失性類型的引用或指針)都會導致未定義的行為。

因此,volatile關鍵字專門用於禁用glvalues上的編譯器優化。 volatile關鍵字在這里可能會影響的唯一事情是可能return x ,編譯器可以使用該函數的其余部分執行任何操作。

編譯器可以優化返回值的多少取決於在這種情況下允許編譯器優化x的訪問量(因為它沒有對任何東西重新排序,嚴格來說,是不刪除return表達式。 ,但是它正在讀寫堆棧,應該可以簡化程序。)因此,在我閱讀該書時,這是允許編譯器優化的灰色區域,並且可以很容易地以兩種方式爭論。

旁注:在這些情況下,請始終假定編譯器將執行與您想要/需要的相反的操作。 您應該禁用優化(至少對於該模塊而言),或者嘗試為所需的內容找到更定義的行為。 (這也是為什么單元測試如此重要的原因)如果您認為它是一個缺陷,則應與C ++開發人員聯系。


這一切仍然很難閱讀,因此請嘗試包含我認為相關的內容,以便您自己閱讀。

glvalue glvalue表達式是lvalue或xvalue。

特性:

可以通過左值到右值,數組到指針或函數到指針的隱式轉換將glvalue隱式轉換為prvalue。 glvalue可能是多態的:它標識的對象的動態類型不一定是表達式的靜態類型。 在表達式允許的情況下,glvalue可以具有不完整的類型。


xvalue以下表達式是xvalue表達式:

函數調用或重載的運算符表達式,其返回類型是對對象的右值引用,例如std :: move(x); a [n],內置的下標表達式,其中一個操作數是一個數組rvalue; am,對象表達式的成員,其中a是一個右值,m是非引用類型的非靜態數據成員; a。* mp,對象表達式成員的指針,其中a是右值,mp是數據成員的指針; 一種 ? b:c,某些b和c的三元條件表達式(有關詳細信息,請參見定義); 轉換表達式以右值引用對象類型,例如static_cast(x); 在臨時實現之后,指定臨時對象的任何表達式。 (自C ++ 17起)屬性:

與右值相同(如下)。 與glvalue相同(如下)。 特別是,像所有右值一樣,x值綁定到右值引用,並且像所有glvalue一樣,x值可能是多態的,非類xvalue可能是cv限定的。


左值以下表達式是左值表達式:

變量,函數或數據成員的名稱,無論類型如何,例如std :: cin或std :: endl。 即使變量的類型是右值引用,包含其名稱的表達式也是左值表達式; 函數調用或重載的運算符表達式,其返回類型為左值引用,例如std :: getline(std :: cin,str),std :: cout <<,str1 = str2或++ it; a = b,a + = b,a%= b以及所有其他內置賦值和復合賦值表達式; ++ a和--a,內置的預遞增和遞減表達式; * p,內置的間接表達式; a [n]和p [n]是內置的下標表達式,除非a是數組右值(自C ++ 11起); am,對象表達式的成員,除非m是成員枚舉數或非靜態成員函數,或者其中a是右值且m是非引用類型的非靜態數據成員; p-> m,指針表達式的內置成員,除非m是成員枚舉數或非靜態成員函數; a。* mp,對象表達式成員的指針,其中a是左值,mp是數據成員的指針; p-> * mp,指向指針表達式成員的內置指針,其中mp是指向數據成員的指針; a,b是內置的逗號表達式,其中b是左值; 一種 ? b:c,某些b和c的三元條件表達式(例如,當它們都是相同類型的左值時,請參見定義); 字符串文字,例如“ Hello,world!”; 轉換為左值引用類型的表達式,例如static_cast(x); 函數調用或重載的運算符表達式,其返回類型是對函數的右值引用; 一個對函數類型右值引用的強制轉換表達式,例如static_cast(x)。 (自C ++ 11起)屬性:

與glvalue相同(如下)。 可以使用左值的地址:&++ i 1和&std :: endl是有效的表達式。 可以將可修改的左值用作內置賦值和復合賦值運算符的左側操作數。 左值可用於初始化左值引用; 這會將新名稱與表達式標識的對象相關聯。


假設規則

只要滿足以下條件,就允許C ++編譯器對程序進行任何更改:

1)在每個序列點,所有易失性對象的值都是穩定的(以前的評估已完成,新評估未開始)(直到C ++ 11)1)嚴格根據語義對易失性對象進行訪問(讀取和寫入)它們出現在其中的表達式。 特別是,它們不會相對於同一線程上的其他易失性訪問而重新排序。 (自C ++ 11起)2)在程序終止時,寫入文件的數據與程序按寫入的方式執行完全相同。 3)在程序等待輸入之前,將顯示發送到交互式設備的提示文本。 4)如果支持ISO C編譯指示#pragma STDC FENV_ACCESS並將其設置為ON,則可以保證浮點算術運算符和函數可以觀察到浮點環境的變化(浮點異常和舍入模式)。盡管執行了上面的中間結果,但調用與調用一樣,只是執行除調用和賦值以外的任何浮點表達式的結果可能具有與該表達式類型不同的浮點類型的范圍和精度(請參閱FLT_EVAL_METHOD)。可以計算任何浮點表達式的整數,就好像是無限范圍和精度一樣(除非#pragma STDC FP_CONTRACT為OFF)


如果您想閱讀規格,我相信這些是您需要閱讀的

參考文獻

C11標准(ISO / IEC 9899:2011):6.7.3類型限定符(p:121-123)

C99標准(ISO / IEC 9899:1999):6.7.3類型限定符(p:108-110)

C89 / C90標准(ISO / IEC 9899:1990):3.5.3類型限定符

我想我從未見過使用volatile的局部變量,它不是指向volatile的指針。 如:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

我所知道的volatile的其他唯一情況是使用在信號處理程序中編寫的全局變量。 那里沒有指針。 或者訪問鏈接描述文件中定義的符號,這些符號位於與硬件相關的特定地址。

在那里,為什么優化會改變可觀察的效果要容易得多。 但是,相同規則適用於本地volatile變量。 編譯器必須表現得好像可以訪問x並且無法對其進行優化。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM