yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

volatile教、あるいはvolatile狂

かつてのMicrosoft Visual Studio .NET 2003のC/C++コンパイラ(MSVC7.1)には、「volatile変数にオレオレ定義の意味を与えて最適化を行う」というアグレッシブすぎるオプションが存在したという昔話。

どんなもの?

このオプションでは、volatileキーワードにC/C++標準規格とは全く異なる独自の意味を与えて、コード生成時の最適化処理に利用します。つまり、コンパイル時に下記を前提としたコード生成を行うのです。

  • 通常の変数=変数実体にアクセスする手段は1通りだけ(aliasingが無い)
  • volatile変数=上記制約の範囲外(aliasingが有る)

これは「volatile=aliasingが有りえる」というMSVC7.1だけの独自拡張です。*1

このaliasing(エイリアシング; 別名)という単語、少々耳慣れないかもしれません。aliasingが無いとは、“ある変数が指す実体にアクセス”するとき、その実体への“別の変数経由でのアクセス”が存在しないことを意味します。具体例を挙げると、int a = 0;に対してint *p = &a;を定義したとき、a*pは“同一の実体(メモリ)”を指します。その後の処理でa = 42;int b = *p;のように異なる変数を介してアクセスするとき、この実体に対してはaliasingが有ると言えます。*2

実はこのaliasingの有無は、コンパイラの最適化処理にとって非常に厄介なものです。通常はaliasingが有ると仮定を置いたコード生成を行わざるをえず、積極的な最適化ができないというケースが多々あります。*3

具体的には何をするの?

この最適化オプションの働きについて、マニュアル(MSDN)を引いてみましょう。

/Oa または /Ow を使用する場合は、次の規則に従う必要があります。次の規則は、volatileとして宣言されていないすべての変数に適用されます。

  • 直接使われている変数をポインタで参照しません。変数が代入式の左辺または右辺で使われているか、あるいは関数の引数として使われていると、その変数が参照されます。
  • 変数へのポインタが使われている場合は、その変数を直接使用できません。
  • 変数のアドレスを関数で取る場合は、その変数を直接使用できません。
  • ポインタによってメモリ位置が変更された場合は、別のポインタでそのメモリ位置にアクセスできません。

上記の規則に従わないと、データが破損する可能性があります。

/Oa、/Ow (エイリアスを使わないと仮定、関数呼び出しでエイリアスを使うと仮定)

…上記のMSDN日本語訳では意味が取りづらい気がしますので、素直に英語版MSDNを参照すべきかもしれません。

If you use /Oa or /Ow, you must follow these rules. The following rules apply for any variable not declared as volatile:

  • No pointer can reference a variable that is used directly (a variable is referenced if it is on either side of an assignment or if a function uses it in an argument).
  • No variable can be used directly if a pointer to the variable is being used.
  • No variable can be used directly if its address is taken within a function.
  • No pointer can be used to access a memory location if another pointer modifies the same memory location.

Failing to follow these rules can cause corrupted data.

/Oa, /Ow (Assume No Aliasing, Assume Aliasing Across Function Calls)

ちょっと何言ってるか分からない

MSDNに掲げられた4つのルールに従うと、下記ソースコードを/Oaオプションありでコンパイルした場合、実行時にデータが壊れて悲惨な結果が得られるのことです(おそらく、予期しない値を読み込んだように見えるでしょう)。いずれもC/C++言語ではごくありふれた処理ですし、このようなソースコードが正しく実行できないとなると、もはや普通のプログラムを書ける気がしません。

int x = 0;
int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // NG: ポインタが参照してはダメ!
}
int x = 0, y;
int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // NG: 直接xを使ってはダメ!
}
int x = 0;
void rule3()
{
  int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // NG: 直接xを使ってはダメ!
}
int x = 0, y;
int *p1 = &x;
int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // NG: 別ポインタ経由で使ってはダメ!
}

volatile教のすゝめ

MSVC7.1曰く、/Oaオプションありでも正常動作させるならば、volatileキーワードを利用せよと。ここまで来ると、もはやvolatile教信者というよりvolatile狂信者の様相を呈しています。

volatile int x = 0;
volatile int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // ポインタが参照してもOK!
}
volatile int x = 0, y;
volatile int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // 直接xを使ってもOK!
}
volatile int x = 0;
void rule3()
{
  volatile int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // 直接xを使ってもOK!
}
volatile int x = 0, y;
volatile int *p1 = &x;
volatile int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // 別ポインタ経由で使ってもOK!
}

Don't Try This At Home, I'm A Professional Volatileian.

その後…

結局のところ、実プロダクトでこの最適化オプションを活用できる場面が無かったのでしょう。次バージョンMicrosoft Visual Studio .NET 2005(MSVC8.0)ではあっさり削除されましたとさ。

  • /Oaコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
  • /Owコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
Visual Studio 2005 - コンパイラの新機能

R.I.P to /Oa, /Ow options.

*1:C/C++標準規格では(主として)「volatile=C/C++実行環境の“外”にリンクしている」を意味し、代表的な例ではメモリマップドI/Oなどでvolatile変数が利用されます。よく「volatile=最適化の抑止」のような表現がされますが、これは「volatile変数アクセス⇒C/C++言語仕様の“外”への干渉/未知の“何か”を操作する⇒コンパイラが直接知りえない操作は勝手に最適化できない」から帰結される一側面に過ぎません。

*2:本記事中では言及しませんが、C言語(C99以降)にはrestrictキーワードが導入されており、この最適化オプションが目指していた事をより安全に達成できます。つまり、プログラマ自身が「aliasingが無い」と分かっている箇所のみを明示し、普通の変数アクセスでは従来通り「aliasingが有る」という仮定が置かれます。詳細はrestrictキーワード続 restrictキーワードなどを参照ください。

*3:C/C++言語の文脈では“Strict Aliasing Rule”という規則があり、これにより一定の条件下ではaliasingが無いと仮定した積極的な最適化処理ができます。詳細は翻訳記事“C/C++のStrict Aliasingを理解する”あたりをどうぞ。