C言語のあれらの小さい秘密のvolatile


volatileの重要性は埋め込み式のプログラマーにとって言うまでもなく、volatileに対する理解度は多くの会社が埋め込み式のプログラマーを募集して面接する時に応募者が合格するかどうかを測る参考基準の一つとして、なぜvolatileが重要なのか.これは、組み込み型のプログラマは、割り込みや最下位のハードウェアなどとよく付き合うため、volatileを使用するため、組み込み型プログラマはvolatileの使用を把握しなければならないからです.
実は読者がよく知っているconstのように、volatileはタイプの修飾子です.volatileの説明を始める前に、次に使用する関数について説明します.この関数の使い方を知っている読者は、この関数の説明部分をスキップすることができます.
プロトタイプ:int gettimeofday(struct timeval*tv,struct timezone*tz);
ヘッダファイル:#include
機能:現在時刻の取得
戻り値:0が正常に返された場合、失敗は-1を返し、エラーコードはerrnoに保存されます.
gettimeofday()は現在の時間をtvが指す構造で返し,現地タイムゾーンの情報はtzが指す構造に格納する.
timeval     :
struct timeval{
 	long tv_sec; 
 	long tv_usec; 
};
timezone      :
struct timezone{
 	int tz_minuteswest; 
 	int tz_dsttime; 
};

まずtimeval構造体のtv_についてお話ししますsecは秒で保存され、tv_usecはマイクロ秒で保存されています.その中のtimezoneメンバー変数はあまり使われていません.ここで簡単に言えばgettimeofday()関数での役割は、現地のタイムゾーンの情報をtzが指す構造に置くことです.ここでtz_minuteswest変数に格納されているのはGreenwichとの時間の差です.tz_dsttime日光で時間を節約した状態.ここでは主に前のメンバー変数timevalに注目し,後のメンバーはここでは使用しないのでgettimeofday()関数を使用するときにパラメータをNULLに設定し,簡単なコードを見てみましょう.
#include <stdio.h>
#include <sys/time.h>

int main(int argc, char * argv[])
{
	struct timeval start,end;
	gettimeofday( &start, NULL );  /*      */
	double timeuse;
	int j;
	for(j=0;j<1000000;j++)
		;
	gettimeofday( &end, NULL );   /*      */
	timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_sec - start.tv_sec ;
	timeuse /= 1000000;
printf("     :%f
",timeuse); return 0; }

実行結果:
root@ubuntu:/home# ./p
     :0.002736

コードを簡単に分析し、end.tv_sec - start.tv_sec終了時間と開始時間の秒単位の時間間隔を得てendを用いる.tv_sec - start.tv_secは、終了時間と開始時間との微妙な単位の時間間隔を得る.時間単位のため、ここでは(end.tv_sec-start.tv_sec)で得られた結果に100000を乗じてマイクロ秒に変換して計算し、timeuse/=100000を使用します.秒に変換します.gettimeofday()関数を使用してstartからendコードまでの実行時間をテストする方法について説明しました.次にvolatile修飾子を見てみましょう.
通常、コードでは、変数が予想外に変化することを防止するために、変数をvolatileとして定義します.これにより、コンパイラはこの変数の値を勝手に「動かす」ことができません.正確には、レジスタに保存されたバックアップではなく、この変数を使用するたびにメモリからこの変数の値を直接読み出す必要があります.
例を挙げる前に、デバッグとReleaseモードでのコンパイル方法の違いを大まかに説明します.デバッグは通常、デバッグバージョンと呼ばれ、デバッグ情報が含まれており、プログラマがプログラムをデバッグするのに便利です.Releaseはパブリケーション・バージョンと呼ばれ、ユーザーがよく使用できるように、プログラムがコード・サイズと実行速度で最適化されるように、さまざまな最適化が行われています.DebugとReleaseの違いを大まかに知った後、コードを見てみましょう.
#include <stdio.h>

void main()
{
int a=12;
printf("a   :%d
",a); __asm {mov dword ptr [ebp-4], 0h} int b = a; printf("b :%d
",b); }

先に上のコードを分析して、私達は1つの__を使いましたasm{mov dword ptr[ebp-4],0 h}で変数aのメモリ内の値を修正し、このコードの機能が不明な読者がいたら、私の前の「C言語の小さな秘密のスタック」を参考にして、ここではあまり説明しません.前にDebugとReleaseのコンパイル方法の違いを説明しましたが、結果を比較してみましょう.注意:vc 6コンパイル実行を使用します.特に説明がない場合は、linux環境でコンパイル実行します.読者自身がコンパイルするときにコンパイル実行のモードを選ぶことを忘れないでください.
Debugモードを使用した結果:
a   :12
b   :0
Press any key to continue

Releaseモードを使用した結果は次のとおりです.
a   :12
b   :12
Press any key to continue

上記の実行結果を見ると,Releaseモードを最適化した後のbの値は12であったが,Debugモードを用いた場合のbの値は0であった.なぜこんなことになったのでしょうか.答えは言わないで、次のコードを見てみましょう.注意:vc 6コンパイルを使用して実行
#include <stdio.h>

void main()
{
int volatile a=12;
printf("a   :%d
",a); __asm {mov dword ptr [ebp-4], 0h} int b = a; printf("b :%d
",b); }

Debugモードを使用した結果:
a   :12
b   :0
Press any key to continue

Releaseモードを使用した結果は次のとおりです.
a   :12
b   :0
Press any key to continue

この場合,Debugモードを用いてもReleaseモードを用いても同じ結果になることを見出した.ここでは、DebugとReleaseモードでのコンパイル方法の違いを分析します.
先に前のコードを分析して、Debugモードの下で私達はコードに対して最適化を行っていないため、コードの中で毎回a値を使う時すべてそのメモリアドレスから直接読み取ったので、私達は__を使いましたasm{mov dword ptr[ebp-4],0 h}文がaの値を変更した後、次にaの値を使用するときにメモリから直接読み出すので、更新後のaの値が得られる.しかし,我々がReleaseモードで実行すると,コンパイラが最適化の過程で最適化処理を行ったため,bの値は我々が更新したaの値ではなくaの前の値であることが分かった.コンパイラは、aに値を付与した後にaの値を変更しないことを発見したので、コンパイラはaの値を1つのレジスタにバックアップし、その後の操作で再びaの値を使用するときに、aのメモリアドレスを読み取るのではなく、このレジスタを直接操作します.なぜなら、メモリを直接読み取る速度が速いからです.これにより,読み出されたa値は前の12となる.更新後の0ではありません.
2番目のセグメントコードでは、volatile修飾子を使用します.この場合、どのモードでも更新後のaの値が得られます.volatile修飾子の役割は、コンパイラが修飾した変数を最適化しないように、値を取るたびにメモリアドレスから直接得ることです.ここから、私たちのコードの易変数については、volatile修飾を使用して、更新するたびに値を得ることが望ましいことがわかります.皆さんの印象を深めるために、次のコードを見てみましょう.
#include <stdio.h>
#include <sys/time.h>

int main(int argc, char * argv[])
{
	struct timeval start,end;
	gettimeofday( &start, NULL );  /*      */
	double timeuse;
	int j;
	for(j=0;j<10000000;j++)
		;
	gettimeofday( &end, NULL );   /*      */
	timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_usec -start.tv_usec;
	timeuse /= 1000000;
printf("     :%f
",timeuse); return 0; }

以前のテスト時間のコードと同様に,for()ループの回数を増やしただけである.
まず、最適化を使用しない結果を見てみましょう.
root@ubuntu:/home# gcc time.c -o p
root@ubuntu:/home# ./p
     :0.028260

最適化された実行結果が使用されました:
root@ubuntu:/home# gcc -o p time.c -O2
root@ubuntu:/home# ./p
     :0.000001

結果から明らかな差はこのように大きいが、上のコードでint jをint volatile jに変更した後、次のコードを見てみましょう.
#include <stdio.h>
#include <sys/time.h>

int main(int argc, char * argv[])
{
	struct timeval start,end;
	gettimeofday( &start, NULL );  /*      */
	double timeuse;
	int volatile j;
	for(j=0;j<10000000;j++)
		;
	gettimeofday( &end, NULL );   /*      */
	timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_usec -start.tv_usec;
	timeuse /= 1000000;
printf("     :%f
",timeuse); return 0; }

まず、最適化を使用しない実行結果を見てみましょう.
root@ubuntu:/home# gcc time.c -o p
root@ubuntu:/home# ./p
     :0.027647

最適化を使用した実行結果は次のとおりです.
root@ubuntu:/home# gcc -o p time.c -O2
root@ubuntu:/home# ./p
     :0.027390

最適化文を用いて実行するかどうかにかかわらず,時間はほとんど変化せず,わずかな差があるだけで,このわずかな差はコンピュータ自体によるものであることが分かった.したがって、volatileを使用していない変数と次のvolatileを使用していない変数の比較結果から、volatileを使用した変数は、for()ループを使用しても最適化されていないことがわかります.for()ループは空の操作であるため、通常、最適化文を使用してこのfor()ループを最適化し、実行しません.コンパイラがコンパイル中にiの値を1000000以上の数に設定し、for()ループ文が実行されないようにします.しかしvolatileを用いることで,コンパイラがi値を勝手に動かさないため,ループが実行される.この例を挙げる理由は、volatile変数を定義した場合、コンパイラによって最適化されないことを読者に覚えてもらうためです.
もちろんvolatileには注意すべき点がありますか?アクセスレジスタの速度は直接メモリにアクセスする速度よりも速いため、コンパイラはメモリへのアクセスを減らすのが一般的ですが、変数にvolatile修飾を加えると、コンパイラはこの変数の読み書き操作が最適化されないことを保証します.これは抽象的かもしれませんが、次のコードを見て、ここで簡単にいくつかのステップを書きます.
main()
{
        int i=o;
        while(i==0)
        {
                 ……
        }
}
以上のコードを解析すると、whileサイクル構造でiの値を変更しなければ、コンパイラはコンパイル中にiの値を1つのレジスタにバックアップし、判断文を実行するたびにそのレジスタから値を取ると、これはデッドサイクルになりますが、次のような修正を行います.
main()
{
        int volatile i=o;
        while(i==0)
        {
                 ……
        }
}
私たちはiの前にvolatileを加えて、while()ループの中で実行されているのはまったく同じ操作であると仮定しますが、このときはデッドサイクルとは言えません.コンパイラは私たちのi値を「バックアップ」操作しないので、判断を実行するたびにiのメモリアドレスから直接読み取ります.値が変化するとループを終了します.
最後に、実際の使用でvolatileの使用の場合、大体以下の点を示します.
1、中断サービスプログラムで修正した他のプログラムが検出するための変数はvolatileを加える必要がある.
2、マルチタスク環境の下で各タスク間で共有するフラグはvolatileを加えるべきである.
3、メモリマッピングのハードウェアレジスタは、通常、読み書きのたびに異なる意味を持つ可能性があるため、volatileを加えて説明する.
volatileの説明はここで終わります.本人のレベルが限られているため、ブログの中の不適切さや間違いは避けられないので、読者の批判と指摘を切に望んでいます.また、読者の皆さんが関連内容を検討することを歓迎します.喜んで交流すれば、貴重な意見を残してください.