C++ エイリアスと最適化の危険な関係
エイリアス
エイリアス(alias) とは別名という意味です
何の別名かというと lvalue の別名ということです
他のオブジェクトのインスタンス(実体)を参照しているものがエイリアスです
文法的にはポインタと参照がエイリアスです
lvalue と rvalue
エイリアスが lvalue の別名であるといいましたが、この lvalue とは何でしょう
かつて lvalue と rvalue というのは代入演算子の左辺か右辺というくらいの意味しかありませんでした
しかし今はそれぞれ独立した概念となっていて次期標準ではさらに細分化されます
現在の標準においては大雑把にいえば lvalue というのは名前の付いたオブジェクトのインスタンスのことで、rvalue というのは名前の無い一時的なもののことです
strict-aliasing-rules
標準では 3.10.15 でエイリアスについての言及があります
3 Basic concepts 3.10 Lvalues and rvalues 15 If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined. (52 - the dynamic type of the object, - a cv-qualified version of the dynamic type of the object, - a type similar (as defined in 4.4) to the dynamic type of the object, - a type that is the signed or unsigned type corresponding to the dynamic type of the object, - a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object, - an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, a an element or non-static data member of a subaggregate or contained union), - a type that is a (possibly cv-qualified) base class type of the dynamic type of the object, - a char or unsigned char type. 52) The intent of this list is to specify those circumstances in which an object may or may not be aliased.
プログラムがオブジェクトに格納されている値に以下の型以外の lvalue を介してアクセスした場合の動作は未定義とする
(注: このリストはどのような状況においてオブジェクトにエイリアスがあるのかどうかを明確にするものである)
- オブジェクトの動的な型そのものでアクセスする場合があるのは当然
- const や volatile は違っていても構わない
- signed や unsigned も違っていても構わない
- signed や unsigned と const や volatile の両方が違っていても構わない
- 上記の型を要素とする配列を介してアクセスしても構わない
また上記の型をメンバとする構造体や union を介してアクセスしても構わない - オブジェクトの動的な型の基底クラスを介してアクセスしても構わない
- char あるいは unsigned char を介してアクセスしても構わない
○ signed や unsigned の有無は気にしない
int n = 10; unsigned int* pu = reinterpret_cast<unsigned int*>(&f); // pu は合法なエイリアス unsigned int u = *pf; // 問題なし
○ char 単位でのアクセスは常に合法
float f = 1.0f; char* pc = reinterpret_cast<char*>(&f); // pc は合法なエイリアス char c = *pc; // 問題なし
× signed char と char は関係ない型なのでダメ。かな?
float f = 1.0f; signed char* pc = reinterpret_cast<signed char*>(&f); // pc は不正なエイリアス signed char c = *pc; // 不正エイリアスを介したアクセスは未定義動
× こんなのはダメ
float f = 1.0f; int* pi = reinterpret_cast<int*>(&f); // pi は不正なエイリアス int i = *pi; // 不正エイリアスを介したアクセスは未定義動作
これを strict-aliasing-rules といいます
strict-aliasing-rules を守るには
strict-aliasing-rules を守るには格納されている値の型と無関係な型でアクセスしなければいいということなので、ある関数内の閉じた文脈については下記のようなコードが無ければ安全です
- reinterpret_cast で無関係な型にキャストしている場合は危険です
- Cスタイルのキャストで reinterpret_cast 相当の挙動を期待している場合も reinterpret_cast と同様に危険です
- void* を介するコードはそこで型の関連性のチェックが切れてしまうのでこれも reinterpret_cast と同様に危険です
それ以外では次のようなものがあると危険です
- memset などのメモリブロック操作系の関数
- malloc などのヒープ関連
- ファイルやネットワークなどとの入出力
- 可変長引数
strict-aliasing-rules というのは格納された値と違う型では一切アクセスしてはいけないというルールなので、ある範囲のデータを C++ の型とは関係なく一塊のバイナリとして扱うようなコードを呼び出しているところは全部ダメです
strict-aliasing-rules が適用されて問題のないコードというのが非常に限られたものであることがわかると思います
strict-aliasing-rules を前提とした最適化
コンパイラによっては strict-aliasing-rules を前提とした最適化をしてくれるオプションがあったりしますが、この strict-aliasing-rules を前提とした最適化は有効である半面非常に危険である場合があるので、この最適化を適用する範囲についてはよくよく検討する必要があります
オプティマイザが strict-aliasing-rules に従っているコードが渡されてくることを期待しているのにそうでないコードを渡してしまった場合、意図しないコードが生成される場合があります
安易に全体に対してこの最適化を適用しないように気をつけてください
でも
このように strict-aliasing-rules の下に於いてはメモリブロックをコピーすることや、オブジェクトのアドレスを元の型と関係ない型のポインタに入れたりすること自体が不法行為のように扱われますが、標準ではもちろんどちらも何の問題もないと明記されています
例えば trivially copiable
3 Basic concepts 3.9 Types 2 For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char. (39 If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value. [ Example: #define N sizeof(T) char buf[N]; T obj; // obj initialized to its original value std::memcpy(buf, &obj, N); // between these two calls to std::memcpy, obj might be modified std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type holds its original value end example ] 39) By using, for example, the library functions (17.6.1.2) std::memcpy or std::memmove.
memcpy と memmove の中身がバイト単位の転送じゃないといけないなんて仕様はありません
では strict-aliasing-rules の下で一体どのように実装すればいいというのでしょうか
ヒープの実装も可変長引数の実装もそうです。一体どのように実装すればいいというのでしょう
strict-aliasing-rules に char 単位のアクセスを認めるという項目があることからわかるように、あのリストは基本的にはエンディアンが念頭にあってのリストであって、4バイトの値を書いたときにどっちのワードが先に置かれるのかというのは未定義であるというようなことを言っているだけだと考えられます
考えられます。というのは個人的な見解ですけど
でもそうでないと memcpy も memset も可変長引数もヒープも何も使えないことになってしまいますからね
それでも
それでも現実問題として一部のコンパイラのオプティマイザはあのリストを錦の御旗に、不正なコードを出力しますので、strict-aliasing-rules を前提とした最適化には十分に気をつけてください
たぶん
たぶん strict-aliasing-rules は標準の不備なんだと思う
ちょっと言葉が足りないんじゃないかな