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