2009-09-12

C++のvolatileについて

ある朝、hitoが不安な夢からふと覚めてみると、2chのC++0xスレで、volatileに関する不毛な話題が繰り広げられているのに気がついた。

C++0x 6

各参考書における、volatileのぞんざいな扱いと、アトミックな操作が可能な処理系では、大抵、処理系独自の方法が用意されていることを考えれば、volatileなど何の役にも立たない無駄機能であることなど、すぐに察せられるのではないだろうか。もし、volatileが本当に役に立つのであれば、参考書はこぞって詳しく取り上げているはずだし、各処理系は、わざわざ独自の方法など用意しなくても良さそうなものである。

どうせ無益だとは思うが、こういう時は、規格を読むのが一番早い。volatileの挙動については、1.9 Program execution [intro.execution]に書いてある。そのほかの箇所に書いてあるのは、型としてどのように振る舞うかという問題だ。

1.9のパラグラフのうち、p1からp6までは、"abstract machine" とやらの定義と、処理系がどのように追随すればいいかということを規定している。それによると、処理系の実装は、見た目が同じならば、どのように実装しても良いらしい。ここまでは、volatile限定ではない一般的な話だ。

さて、p7に、いよいよvolatileの事が書いてある。それによれば、

volatileなlvalueであると示されているオブジェクトへのアクセス、オブジェクトの変更、ライブラリのI/O関数、また、それらの操作を行う関数の呼び出しは、すべて副作用である。すなわち、実行環境の状態を変える事を言う。式の評価は副作用を引き起こすかもしれない。シークエンスポイントと呼ばれる、実行順序のある点において、それまでの評価による副作用はすべて完了していなければならないし、後続の評価によってもたらされる副作用が起きてはならない。

ところで、これはC++03の文面である。ご存じの通り、C++03には、マルチスレッドという概念そのものが存在しない。よって、この規定も、スレッドがない環境のことしか考えていないのだ。

よく分からないのでコードを書いてみた。

void f()
{
    volatile int x = 0 ;
    // この時点で、xはすでに0である。
    ++x ; // #1
   // この時点で、xはすでに1である。まだ2ではない。
    int y = ++x ; // #2
}

#2が評価される時点で、すでに#1の評価と、それに伴う副作用の適用は、すべて終了してますよということだろうか。でもこれは、volatileがなくても当然じゃなかろうか。スレッドがない環境では、これぐらいしか思いつかない。他には例えば、翻訳単位が複数あり、レジスタというものが存在する処理系で、externを使って外部の変数を参照していた時、常にレジスタ上に変数を置いていては、メモリ上の値と同期しない場合がある。そういう場合においても、動作を保証するだとかだとか、そういうこともあるのかもしれない。その場合は、volatileの変数に対する積極的な最適化をしない方向で実装されるだろうと思う。volatileが、積極的な最適化をしないという修飾子だという迷信が広がっているのは、このため歟。何にせよ、処理系に依存しすぎていて、純粋な規格の範疇からは外れてしまう。

スレッドは存在しないが、割り込みはある。p9に規定されている。割り込みが起こった時点で、volatile sig_atomic_t以外の型のオブジェクトの値は、不定であり、volatile sig_atomic_t以外の型のオブジェクトを変更した場合も、未定義である。

つまりはこういうことだろう。

volatile int x ;
volatile sig_atomic_t y ;

// 割り込み用の関数
// 終了したら、割り込み時の実行に戻る。
void interrupt_handler()
{
    ++x ; ++y ;
}

void f()
{// f()の実行中にinterrupt_handler()による割り込みが実行されるものとする。
    for ( int i = 0 ; i != 10 ; ++i )
    { ++x ; ++y ; }
}

void g()
{
    x = 0 ; y = 0 ;
    f() ;

// xの値は未定義
// yの値は11
}

割り込みならば意味があるが、型がvolatile sig_atomic_tでなければならない。

もう一度言うが、スレッドがどういうものなのか、C++03は知らない。もちろん、処理系はこれらの挙動と同じであれば、それでいい。それ以上のことをしてもかまわないわけだ。例えば、型がvolatile sig_atomic_tでなくても、割り込み時にアトミックな操作が保証されるだとか、スレッドというものが存在する処理系においては、スレッド間でもatomicな動作を保証するだとか、そういう風に実装するのは、処理系の自由である。ただし、実装しなくても、一向にかまわない。

ちなみに、C++0xでは、p7相当の文面が消えてしまっている。たぶん、registerと同じで、あまり役に立たなかったからだろう。
p13に残っていた。ただ、文面がだいぶ変わっている。これ、使い物になるんだろうか。

そもそも、C++0xでは、アトミックな操作が標準ライブラリに取り入れられているので、volatileなどという、不透明な修飾を用いる必要などないだろう。

4 comments:

Anonymous said...

> volatileなlvalueへの、副作用を伴う操作において、
volatile な lvalue を通したオブジェクトへのアクセス自体が
「副作用」のひとつとして定義されているので、それが副作用を
伴わないことがあるかのような上記の記述はおかしいです。

> C++0xでは、p7相当の文面が消えてしまっている。
ドラフト N2914 だと p13 以降に移っているようです。

江添亮 said...

やはりツッコミが来ましたか。
この辺りの事は、やはり難しい。

Anonymous said...

主にメモリマップドI/Oのためにvolatileが用意されているのではないかと思います。

volatile int *p = (int*) (0x4000);
int a = *p;
int b = *p;

というコードがあった場合、pへのメモリアクセスに副作用がありますので、次のように最適化されてしまうと実行結果が変わってしまいます。

int a = *p;
int b = a;

Anonymous said...

そもそも volatile と読み書きのアトミック性や並列動作とは
関係ないので、それらの点だけについて必要性を考えれば
意味が無いものとなってしまうのは当然。

volatile はその名のとおり「揮発性」ということであり、
C/C++ の仮想機械の外部にある要因で値が変わる可能性がある
ということが原義で、そのために読み書きを省略したり順番を
入れ替えたりできないことになっている。

この性質自体はメモリマップド I/O の操作を C/C++ で記述する
ために C でも C++03 でも C++0x でもずっと必要。