簡體   English   中英

在C ++中忽略了揮發性說明符

[英]Volatile specifier ignored in C++

我很新的C ++,最近我碰到一些信息跑意味着什么變量是volatile 據我了解,這意味着對變量的讀取或寫入永遠不會被優化。

但是,當我聲明一個不大1、2、4、8字節的volatile變量時,就會出現一種奇怪的情況:編譯器(啟用了C ++ 11的gnu)似乎忽略了volatile說明符

#define expand1 a, a, a, a, a, a, a, a, a, a
#define expand2 // ten expand1 here, expand3 to expand5 follows
// expand5 is the equivalent of 1e+005 a, a, ....

struct threeBytes { char x, y, z; };
struct fourBytes { char w, x, y, z; };

int main()
{
   // requires ~1.5sec
   foo<int>();

   // doesn't take time
   foo<threeBytes>();

   // requires ~1.5sec
   foo<fourBytes>();
}

template<typename T>
void foo()
{
   volatile T a;

   // With my setup, the loop does take time and isn't optimized out
   clock_t start = clock();
   for(int i = 0; i < 100000; i++);
   clock_t end = clock();
   int interval = end - start;

   start = clock();
   for(int i = 0; i < 100000; i++) expand5;
   end = clock();

   cout << end - start - interval << endl;
}

他們的時間是

  • foo<int>() :約1.5秒
  • foo<threeBytes>() :0

我已經使用1到8個字節的不同變量(無論是否由用戶定義)測試了它,只有1,2,4,8運行時間。 這是僅在我的設置中存在的錯誤,還是對編譯器的請求是volatile的,而不是絕對的?

PS四個字節的版本總是占用其他時間的一半,這也是造成混亂的原因

可能會優化struct版本,因為編譯器意識到,無論volatile如何,都沒有副作用(不會對變量a讀取或寫入)。 基本上,你有一個無操作, a; ,因此編譯器可以根據自己的喜好進行操作; 它不會被迫展開循環或對其進行優化,因此volatile在這里並不重要。 int的情況下,似乎沒有優化,但這與volatile的使用情況一致: 當您可能在其中“訪問對象”(即讀取或寫入)時, 應期望進行非優化。循環。 但是,構成“對對象的訪問”的內容是實現定義的(盡管大多數情況下它遵循常識),請參閱底部的EDIT 3

這里的玩具示例:

#include <iostream>
#include <chrono>

int main()
{
    volatile int a = 0;

    const std::size_t N = 100000000;

    // side effects, never optimized
    auto start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        ++a; // side effect (write)
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;

    // no side effects, may or may not be optimized out
    start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        a; // no side effect, this is a no-op
    end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;
}

編輯

這個最小的示例中可以看出,no-op實際上並未針對標量類型進行優化。 對於struct來說, 它已經過優化。 在我鏈接的示例中, clang不會優化而不是優化代碼,而是使用-O3優化兩個循環。 gcc不會沒有優化就優化循環,而是只會在啟用優化的情況下優化第一個循環。

編輯2

clang發出警告: warning: expression result unused; assign into a variable to force a volatile load [-Wunused-volatile-lvalue] warning: expression result unused; assign into a variable to force a volatile load [-Wunused-volatile-lvalue] 因此,我最初的猜測是正確的,編譯器可以優化無操作,但這不是強制性的。 我不了解為什么要對struct而不是標量類型執行此操作,但這是編譯器的選擇,並且符合標准。 由於某種原因,它僅在no-op是struct時才發出此警告,而當它是標量類型時不發出警告。

還要注意,您沒有“讀/寫”功能,只有無操作操作,因此您不應該期望volatile什么用。

編輯3

摘自黃金書(C ++標准)

7.1.6.1/8 cv限定詞[dcl.type.cv]

實現定義是構成對具有volatile限定類型的對象的訪問。 ...

因此,由編譯器決定何時優化循環。 在大多數情況下,它遵循常識:在讀取或寫入對象時。

這個問題比它最初出現時有趣得多(對於“有趣”的一些定義)。 您似乎發現了一個編譯器錯誤(或有意不符合),但與您期望的不完全相同。

根據標准,您的foo調用之一具有未定義的行為,而另外兩個則格式錯誤。 我將首先解釋應該發生什么。 休息后可以找到相關的標准報價。 出於我們的目的,我們可以分析簡單的表達式語句a, a, a; 給定volatile T a;

此表達式語句中的a, a, a是舍棄值表達式([stmt.expr] / p1)。 表達式a, a, a的類型是右操作數的類型,即id表達式 avolatile T 因為a是一個左值,所以表達式a, a, a ([expr.comma] / p1)也是如此。 因此,此表達式是volatile限定類型的左值,並且是“逗號表達式,其中右操作數是這些表達式之一”,尤其是id表達式 ,因此[expr] / p11需要左值值到值的轉換將應用於表達式a, a, a 類似地,在a, a, a ,左表達式a, a也是一個廢棄值表達式,在該表達式內,左表達式a也是一個廢棄值表達式; 類似的邏輯表明[expr] / p11要求將左值到右值轉換應用於表達式a, a的結果和表達式a (最左邊的一個)的結果。

如果T是類類型( threeBytesfourBytes ),則應用左值到右值轉換需要通過從易失性左值a ([conv.lval] / p2)進行復制初始化來創建一個臨時值。 但是,隱式聲明的副本構造函數始終通過非易失性引用([class.copy] / p8)接受其參數; 這樣的引用不能綁定到易失對象。 因此,該程序格式不正確。

如果Tint ,則應用從左值到右值的轉換將得出a包含的值。 但是,在您的代碼中, a永遠不會初始化; 因此,此評估會產生不確定的值,並且每[dcl.init] / p12都會導致不確定的行為。


標准報價如下。 全部來自C ++ 14:

[expr] / p11:

在某些情況下,表達式僅出於其副作用而出現。 這樣的表達式稱為廢棄值表達式 計算表達式,並舍棄其值。 不應用數組到指針(4.2)和函數到指針(4.3)的標准轉換。 當且僅當表達式是volatile限定類型的glvalue並且它是以下之一時,才應用左值到右值轉換(4.1):

  • expression ),其中expression是這些表達式之一,
  • id-expression (5.1.1),
  • [省略了幾個不適用的項目符號],或
  • 逗號表達式(5.18),其中右操作數是這些表達式之一。

[ 注意 :使用重載運算符會導致函數調用; 以上僅涵蓋具有內置含義的運算符。 如果左值是類類型,則它必須具有易失性副本構造函數以初始化由左值到右​​值轉換的結果的臨時值。 尾注 ]

[expr.comma] / p1:

一對用逗號分隔的表達式從左到右求值; 左邊的表達式是舍棄值表達式(第5條)[...]結果的類型和值是右邊操作數的類型和值; 結果與右操作數具有相同的值類別。

[stmt.expr] / p1:

表達式語句具有以下形式

 expression-statement: expression_opt; 

該表達式是一個廢棄值表達式(第5條)。

[conv.lval] / p1-2:

1非函數,非數組類型T的glvalue(3.10)可以轉換為prvalue。 如果T是不完整的類型,則必須進行這種轉換的程序格式錯誤。 如果T是非類類型,則prvalue的類型是T的cv不合格版本。 否則,prvalue的類型為T。

2 [此處不相關的一些特殊規則]在所有其他情況下,轉換結果都是根據以下規則確定的:

  • [省略不適用的項目符號]
  • 否則,如果T具有類類型,則轉換從glvalue復制初始化T類型的臨時對象,並且轉換結果是該臨時對象的prvalue。
  • [省略不適用的項目符號]
  • 否則,glvalue指示的對象中包含的值就是prvalue結果。

[dcl.init] / p12:

如果未為對象指定初始化程序,則該對象將被默認初始化。 當獲得具有自動或動態存儲持續時間的對象的存儲時,該對象具有不確定的值,並且如果未對該對象執行任何初始化,則該對象將保留不確定的值,直到替換該值為止(5.17)。 [...]如果評估產生不確定的值,則該行為是不確定的,但以下情況除外:[某些與無符號窄字符類型有關的不適用異常]

[class.copy] / p8:

X類隱式聲明的副本構造函數的形式為

 X::X(const X&) 

如果類類型M (或其數組)的每個可能構造的子對象都有一個副本構造函數,其第一個參數的類型為const M&const volatile M& 否則,隱式聲明的副本構造函數將具有以下形式:

 X::X(X&) 

volatile不會執行您認為的操作。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html

如果您在Boehm在我鏈接的頁面上提到的三種非常特定的用途之外依賴於volatile ,那么您將得到意想不到的結果。

暫無
暫無

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

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