volatile
キーワードは何をしますか?C ++では、どのような問題を解決しますか?
私の場合、私は故意にそれを必要としたことはありません。
volatile
でも効果的に使用できる例を示したすばらしいリソースであり、かなり平易な言葉でまとめられています。リンク:publications.gbdirect.co.uk/c_book/chapter8/...
volatile
よりも便利ですfriend
。
volatile
キーワードは何をしますか?C ++では、どのような問題を解決しますか?
私の場合、私は故意にそれを必要としたことはありません。
volatile
でも効果的に使用できる例を示したすばらしいリソースであり、かなり平易な言葉でまとめられています。リンク:publications.gbdirect.co.uk/c_book/chapter8/...
volatile
よりも便利ですfriend
。
回答:
volatile
たとえば、完全に別のプロセス/デバイス/書き込み可能なものからメモリ内のスポットを読み取る場合に必要です。
以前はストレートCのマルチプロセッサシステムでデュアルポートRAMを使用していました。セマフォとしてハードウェア管理の16ビット値を使用して、相手がいつ終了したかを知りました。基本的にこれを行いました:
void waitForSemaphore()
{
volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}
なしvolatile
では、オプティマイザはループを役に立たないと見なし(男は決して値を設定しません!彼は気が狂って、そのコードを削除します!)、私のコードはセマフォを取得せずに続行し、後で問題を引き起こします。
uint16_t* volatile semPtr
代わりにが書き込まれた場合はどうなりますか?これはポインタを(指された値ではなく)揮発性としてマークする必要があるため、ポインタ自体のチェックはsemPtr == SOME_ADDR
最適化されない可能性があります。ただし、これはまた、不安定な先の尖った値を意味します。番号?
volatile
メモリマップされたハードウェアデバイスの読み取りまたは書き込みを行う必要がある組み込みシステムまたはデバイスドライバーを開発する場合に必要です。特定のデバイスレジスタの内容はいつでも変更される可能性があるvolatile
ため、このようなアクセスがコンパイラによって最適化されないようにするには、キーワードが必要です。
一部のプロセッサーには、64ビットを超える精度の浮動小数点レジスターがあります(例えば、SSEなしの32ビットx86、Peterのコメントを参照)。こうすることで、倍精度の数値に対して複数の演算を実行した場合、各中間結果を64ビットに切り捨てた場合よりも、実際には高精度の答えが得られます。
これは通常素晴らしいことですが、コンパイラーがレジスターを割り当て、最適化を行った方法に応じて、まったく同じ入力に対するまったく同じ操作で異なる結果が得られることを意味します。一貫性が必要な場合は、volatileキーワードを使用して、各操作を強制的にメモリに戻すことができます。
また、Kahan加算など、代数的には意味を持たないが浮動小数点エラーを削減する一部のアルゴリズムにも役立ちます。代数的にそれはnopなので、いくつかの中間変数が揮発性でない限り、それはしばしば誤って最適化されます。
volatile double
代わりに宣言することで解決しました。これによりdouble
、FPU精度から64ビット(RAM)精度に切り捨てられてから、さらに計算が続行されます。浮動小数点誤差がさらに誇張されたため、結果は大幅に異なりました。
g++ -mfpmath=sse
それを32ビットx86にも使用するようなオプションがあります。を使用gcc -ffloat-store
して、x87 を使用している場合でも四捨五入を強制できます。または、x87の精度を53ビットの仮数に設定できます:randomascii.wordpress.com/2012/03/21/…。
volatile
して、すべての場所の利点を失うことなく、いくつかの特定の場所で丸めを強制できます。
「約束のように揮発性」ダン・サックスの記事:
(...)揮発性オブジェクトとは、値が自然に変化する可能性があるオブジェクトです。つまり、オブジェクトを揮発性であると宣言すると、プログラム内のステートメントがオブジェクトを変更していないように見えても、オブジェクトが状態を変更する可能性があることをコンパイラーに伝えています。」
volatile
キーワードに関する彼の3つの記事へのリンクは次のとおりです。
ロックフリーのデータ構造を実装する場合は、volatileを使用する必要があります。それ以外の場合、コンパイラは変数へのアクセスを自由に最適化できます。これにより、セマンティクスが変更されます。
言い換えると、volatileは、この変数へのアクセスが物理メモリの読み取り/書き込み操作に対応している必要があることをコンパイラに通知します。
たとえば、これはWin32 APIでInterlockedIncrementを宣言する方法です。
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
std::atomic<LONG>
されたため、純粋なロード/純粋なストアを最適化したり、並べ替えたりするなどの問題を発生させることなく、ロックレスコードをより安全に記述できます。
1990年代初頭に私が使用していた大きなアプリケーションには、setjmpとlongjmpを使用したCベースの例外処理が含まれていました。"catch"句として機能するコードのブロックに値を保存する必要がある変数では、volatileキーワードが必要でした。これらの変数がレジスターに格納され、longjmpによって消去されないようにするためです。
標準Cでは、使用する場所の1つはvolatile
シグナルハンドラーです。実際、標準Cでは、シグナルハンドラーで安全に実行できることは、volatile sig_atomic_t
変数を変更するか、すばやく終了することです。実際、私の知る限り、これは、標準Cで唯一の場所であり、volatile
未定義の動作を回避するために使用する必要があります。
ISO / IEC 9899:2011§7.14.1.1
signal
機能¶5
abort
またはraise
関数の呼び出しの結果として以外にシグナルが発生した場合、値を割り当てる以外の方法で、ロックされていないアトミックオブジェクトではない静的またはスレッドストレージ期間のオブジェクトをシグナルハンドラが参照する場合の動作は未定義です。として宣言されたオブジェクトに対してvolatile sig_atomic_t
、またはシグナルハンドラが、abort
関数、_Exit
関数、quick_exit
関数、またはsignal
関数の呼び出しを引き起こしたシグナルに対応するシグナル番号に等しい最初の引数を持つ関数以外の標準ライブラリ内の関数を呼び出すハンドラ。さらに、そのようなsignal
関数の呼び出しがSIG_ERRを返す場合、の値errno
は不確定です。252)252)非同期シグナルハンドラーによってシグナルが生成された場合の動作は未定義です。
つまり、標準Cでは次のように記述できます。
static volatile sig_atomic_t sig_num = 0;
static void sig_handler(int signum)
{
signal(signum, sig_handler);
sig_num = signum;
}
他にはあまりありません。
POSIXは、シグナルハンドラーで実行できることについてはるかに寛容ですが、それでも制限があります(そして、制限の1つは、標準I / Oライブラリ(printf()
他)を安全に使用できないことです)。
組み込み向けに開発していて、割り込みハンドラーで変更できる変数をチェックするループがあります。「揮発性」がないと、ループは何もしない状態になります。コンパイラーが認識できる限り、変数は変更されないため、チェックは最適化されます。
同じことが、より伝統的な環境の別のスレッドで変更される可能性のある変数にも当てはまりますが、同期呼び出しが頻繁に行われるため、コンパイラーは最適化にそれほど自由ではありません。
意図したとおりに使用することに加えて、揮発性は(テンプレート)メタプログラミングで使用されます。揮発性属性(constなど)が過負荷の解決に関与するため、偶発的な過負荷を防止するために使用できます。
template <typename T>
class Foo {
std::enable_if_t<sizeof(T)==4, void> f(T& t)
{ std::cout << 1 << t; }
void f(T volatile& t)
{ std::cout << 2 << const_cast<T&>(t); }
void bar() { T t; f(t); }
};
これは合法です。両方のオーバーロードは呼び出し可能であり、ほとんど同じです。とにかくvolatile
バーが非揮発性を渡さないことがわかっているので、オーバーロードのキャストは合法T
です。volatile
非揮発性の場合はそのオーバーロードの解決に選ばれたことがない、けれどもバージョンは、厳密に悪化していますf
可能です。
コードが実際にvolatile
メモリアクセスに依存することはありません。
の volatile
キーワードは、コンパイラによって決定することができない方法で変更することができ、オブジェクト上の任意の最適化を適用するから、コンパイラを防ぐためのものです。
として宣言されたオブジェクトはvolatile
、現在のコードのスコープ外のコードでいつでも値を変更できるため、最適化から除外されます。システムvolatile
は、以前の命令が同じオブジェクトからの値を要求した場合でも、要求された時点でその値を一時レジスターに保持するのではなく、常にメモリー位置からオブジェクトの現在の値を読み取ります。
以下のケースを検討してください
1)スコープ外の割り込みサービスルーチンによって変更されたグローバル変数。
2)マルチスレッドアプリケーション内のグローバル変数。
volatile修飾子を使用しない場合、次の問題が発生する可能性があります
1)最適化をオンにすると、コードが期待どおりに機能しない場合があります。
2)割り込みを有効にして使用すると、コードが期待どおりに動作しない場合があります。
https://en.wikipedia.org/wiki/Volatile_(computer_programming)
揮発性キーワードは、変数へのアクセスを最適化しないようにコンパイラーに指示するために使用されるという事実(スレッドまたは割り込みルーチンによって変更される可能性がある)に加えて、いくつかのコンパイラーのバグを削除するためにも使用できます- はい、できますなる ---。
たとえば、組み込みプラットフォームで作業していたのは、コンパイラーが変数の値に関して誤った仮定を行っていたためです。コードが最適化されていない場合、プログラムは正常に実行されます。最適化(これは重要なルーチンだったために本当に必要でした)を使用すると、コードは正しく機能しません。(あまり正確ではありませんが)唯一の解決策は、「障害のある」変数を揮発性として宣言することでした。
あなたのプログラムはvolatile
キーワードがなくても動作するようです?おそらくこれが理由です:
前述のように、volatile
キーワードは次のような場合に役立ちます
volatile int* p = ...; // point to some memory
while( *p!=0 ) {} // loop until the memory becomes zero
ただし、外部関数または非インライン関数が呼び出されると、ほとんど効果がないようです。例えば:
while( *p!=0 ) { g(); }
次に、volatile
ほとんど同じ結果の有無にかかわらず、結果が生成されます。
g()を完全にインライン化できる限り、コンパイラーは進行中のすべてを確認できるため、最適化できます。しかし、プログラムがコンパイラーが何が行われているのかを認識できない場所を呼び出す場合、コンパイラーが想定をこれ以上行うことは安全ではありません。したがって、コンパイラは常にメモリから直接読み取るコードを生成します。
ただし、(明示的な変更またはコンパイラー/リンカーの巧妙さが原因で)関数g()がインラインになると、その日の注意が必要volatile
です。キーワードを忘れた場合、コードが壊れる可能性があります。
したがってvolatile
、プログラムが機能していないように見えても、キーワードを追加することをお勧めします。これにより、将来の変更に関して意図がより明確で堅牢になります。
volatile
修飾された関数ポインタを:void (* volatile fun_ptr)() = fun; fun_ptr();
Cの初期の頃は、コンパイラーはlvalueの読み取りと書き込みを行うすべてのアクションをメモリー操作として解釈し、コードに表示された読み取りと書き込みと同じ順序で実行されていました。多くの場合、コンパイラーに操作の順序を変更して統合する一定の自由度が与えられれば、効率は大幅に向上しますが、これには問題がありました。それらを指定する必要があったためであっても操作は、多くの場合、単に特定の順序で指定されたいくつかのために、したがって、プログラマは必ずしもそうではありませんでした多くの等しく良い選択肢のいずれかを、選びました。特定のシーケンスで特定の操作を実行することが重要な場合があります。
シーケンス処理のどの詳細が重要であるかは、ターゲットプラットフォームとアプリケーション分野によって異なります。標準は、特に詳細な制御を提供するのではなく、単純なモデルを選択しました。修飾されていない左辺値を使用して一連のアクセスが行われたvolatile
場合、コンパイラーは、適切と思われるようにそれらを並べ替えて統合します。volatile
-qualified lvalueを使用してアクションを実行する場合、品質の実装では、非標準の構文を使用する必要なく、目的のプラットフォームとアプリケーションフィールドをターゲットとするコードで必要となる可能性がある追加の順序保証を提供する必要があります。
残念ながら、多くのコンパイラーは、プログラマーが必要とする保証を特定するのではなく、標準によって義務付けられた最低限の保証を提供することを選択しました。これはvolatile
、本来あるべきよりもずっと役に立たなくなります。たとえば、gccまたはclangでは、基本的な「ハンドオフミューテックス」を実装する必要があるプログラマー(ミューテックスを取得して解放したタスクは、他のタスクが完了するまで再度実行しない)は、必ず実行する必要があります。 4つのことの:
mutexの獲得と解放を、コンパイラーがインライン化できず、プログラム全体の最適化を適用できない関数に置きます。
mutexによって保護されているすべてのオブジェクトをvolatile
--mutexを取得してから解放する前にすべてのアクセスが発生する場合は必要のないものとして修飾します。
修飾されていないすべてのオブジェクトがあるかのようにコードを生成するコンパイラを強制するために最適化レベル0を使用しregister
ますvolatile
。
gcc固有のディレクティブを使用します。
対照的に、iccなどのシステムプログラミングにより適した高品質のコンパイラを使用する場合は、別のオプションがあります。
volatile
取得または解放が必要なすべての場所で-qualified書き込みが実行されることを確認してください。基本的な「ハンドオフミューテックス」を取得するには、volatile
読み取りが必要です(準備ができているかどうかを確認するため)。volatile
書き込みも必要ありません(反対側は、返されるまで再取得を試みません)。意味のないvolatile
書き込みを実行することは、gccまたはclangで使用可能なオプションよりも優れています。
すべての答えは素晴らしいです。しかしその上で、私は例を共有したいと思います。
以下は、小さなcppプログラムです。
#include <iostream>
int x;
int main(){
char buf[50];
x = 8;
if(x == 8)
printf("x is 8\n");
else
sprintf(buf, "x is not 8\n");
x=1000;
while(x > 5)
x--;
return 0;
}
ここで、上記のコードのアセンブリを生成します(ここでは、アセンブリの該当部分のみを貼り付けます)。
アセンブリを生成するコマンド:
g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp
そしてアセンブリ:
main:
.LFB1594:
subq $40, %rsp #,
.seh_stackalloc 40
.seh_endprologue
# assembly.cpp:5: int main(){
call __main #
# assembly.cpp:10: printf("x is 8\n");
leaq .LC0(%rip), %rcx #,
# assembly.cpp:7: x = 8;
movl $8, x(%rip) #, x
# assembly.cpp:10: printf("x is 8\n");
call _ZL6printfPKcz.constprop.0 #
# assembly.cpp:18: }
xorl %eax, %eax #
movl $5, x(%rip) #, x
addq $40, %rsp #,
ret
.seh_endproc
.p2align 4,,15
.def _GLOBAL__sub_I_x; .scl 3; .type 32; .endef
.seh_proc _GLOBAL__sub_I_x
sprintf
コンパイラーx
はプログラムの外部で変更されないと想定しているため、アセンブリーでコードが生成されなかったことがアセンブリーでわかります。while
ループについても同様です。while
ループは最適化により完全に削除されました。コンパイラーがループを役に立たないコードと見なし、直接割り当てられた5
ためですx
(を参照movl $5, x(%rip)
)。
問題は、外部プロセス/ハードウェアがとのx
間のどこかの値を変更する場合に発生します。ブロックが機能することを期待しますが、残念ながらコンパイラはその部分を削除しました。x = 8;
if(x == 8)
else
これを解決するために、で、生成されたアセンブリコードにassembly.cpp
変更int x;
してvolatile int x;
、すぐに見てみましょう。
main:
.LFB1594:
subq $104, %rsp #,
.seh_stackalloc 104
.seh_endprologue
# assembly.cpp:5: int main(){
call __main #
# assembly.cpp:7: x = 8;
movl $8, x(%rip) #, x
# assembly.cpp:9: if(x == 8)
movl x(%rip), %eax # x, x.1_1
# assembly.cpp:9: if(x == 8)
cmpl $8, %eax #, x.1_1
je .L11 #,
# assembly.cpp:12: sprintf(buf, "x is not 8\n");
leaq 32(%rsp), %rcx #, tmp93
leaq .LC0(%rip), %rdx #,
call _ZL7sprintfPcPKcz.constprop.0 #
.L7:
# assembly.cpp:14: x=1000;
movl $1000, x(%rip) #, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_15
cmpl $5, %eax #, x.3_15
jle .L8 #,
.p2align 4,,10
.L9:
# assembly.cpp:16: x--;
movl x(%rip), %eax # x, x.4_3
subl $1, %eax #, _4
movl %eax, x(%rip) # _4, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_2
cmpl $5, %eax #, x.3_2
jg .L9 #,
.L8:
# assembly.cpp:18: }
xorl %eax, %eax #
addq $104, %rsp #,
ret
.L11:
# assembly.cpp:10: printf("x is 8\n");
leaq .LC1(%rip), %rcx #,
call _ZL6printfPKcz.constprop.1 #
jmp .L7 #
.seh_endproc
.p2align 4,,15
.def _GLOBAL__sub_I_x; .scl 3; .type 32; .endef
.seh_proc _GLOBAL__sub_I_x
ここではsprintf
、printf
とwhile
ループのアセンブリコードが生成されたことがわかります。利点は、x
変数が外部プログラムまたはハードウェアによって変更された場合sprintf
、コードの一部が実行されることです。そして、同様にwhile
ループは今忙しい待機のために使用することができます。
他の回答では、次の目的でいくつかの最適化を回避することについてすでに言及しています:
揮発性は、値が外部から来て予測不可能であり、既知の値に基づくコンパイラの最適化を回避する必要がある場合、および結果が実際には使用されていないが計算する必要がある場合、または使用されている場合に不可欠です。ベンチマークのために数回計算し、正確なポイントで開始および終了する計算が必要です。
揮発性読み取りは入力操作(のようなscanf
またはその使用cin
)に似ています。値はプログラムの外部から取得されているように見えるため、値に依存する計算は、その後に開始する必要があります。
揮発性書き込みは、出力操作(のようなprintf
またはその使用cout
)に似ています。値はプログラムの外部で通信されるように見えるため、値が計算に依存する場合は、の前に終了する必要があります。
したがって、1組の揮発性読み取り/書き込みを使用してベンチマークを制御し、時間測定を意味のあるものにすることができます。
volatileがなければ、時間測定などの関数を使用した計算の並べ替えを妨げるものがないため、以前にコンパイラーによって計算を開始できます。