web-dev-qa-db-ja.com

なぜ最適化はこの関数を殺すのですか?

私たちは最近、大学でいくつかの言語でのプログラミングスペシャルについて講義をしました。

講師は次の機能を書き留めました。

inline u64 Swap_64(u64 x)
{
    u64 tmp;
    (*(u32*)&tmp)       = Swap_32(*(((u32*)&x)+1));
    (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x);

    return tmp;
}

これも読みやすさの点で本当に悪いスタイルであることを私は完全に理解していますが、彼の主なポイントは、コードのこの部分が高い最適化レベルを有効にするまで、本番コードで正常に機能したことでした。その場合、コードは何もしません。

彼は、変数tmpへのすべての割り当ては、コンパイラーによって最適化されると述べました。しかし、なぜこれが起こるのでしょうか?

変数を宣言する必要がある状況があることを理解していますvolatileコンパイラーは、変数が読み書きされないと思っても変数に触れないようにしますが、なぜこれが発生するのかわかりませんここに。

49
guitarflow

このコードは 厳密なエイリアシングルール に違反しているため、* char **を介したアクセスは許可されていますが、別のタイプのポインタを介してobjectにアクセスすることは違法です。コンパイラーは、異なるタイプのポインターが同じメモリーを指していないと想定し、それに応じて最適化することができます。また、コードが 未定義の振る舞い を呼び出し、実際には何でもできることを意味します。

このトピックの最良のリファレンスの1つは 厳密なエイリアシングの理解 であり、最初の例はOPのコードと同様の流れであることがわかります。

uint32_t swap_words( uint32_t arg )
{
  uint16_t* const sp = (uint16_t*)&arg;
  uint16_t        hi = sp[0];
  uint16_t        lo = sp[1];

  sp[1] = hi;
  sp[0] = lo;

 return (arg);
} 

この記事では、このコードが違反していると説明しています厳密なエイリアスルールspargのエイリアスですが、タイプが異なり、コンパイルされますが、おそらくargは、swap_wordsが戻った後も変更されません。簡単なテストでは、上記のコードでもOPコードでもその結果を再現することはできませんが、これは未定義の振る舞いであり、したがって予測できないため、何の意味もありません。

この記事では、さまざまなケースについて説明し、type-punningを含むいくつかの実用的なソリューションを紹介します。これはC99で明確に定義されています。1C++では定義されていない可能性がありますが、実際にはほとんどの主要なコンパイラでサポートされています。たとえば、ここに 型のパンニングに関するgccのリファレンス があります。前のスレッド CおよびC++でのユニオンの目的 は厄介な詳細に入ります。このトピックには多くのスレッドがありますが、これは最善の仕事をしているようです。

そのソリューションのコードは次のとおりです。

typedef union
{
  uint32_t u32;
  uint16_t u16[2];
} U32;

uint32_t swap_words( uint32_t arg )
{
  U32      in;
  uint16_t lo;
  uint16_t hi;

  in.u32    = arg;
  hi        = in.u16[0];
  lo        = in.u16[1];
  in.u16[0] = lo;
  in.u16[1] = hi;

  return (in.u32);
}

参考までに、 C99ドラフト標準 の関連セクション厳密なエイリアシング6.5段落7それは言う:

オブジェクトの格納値には、次のいずれかのタイプの左辺値式によってのみアクセスできます。76)

—オブジェクトの有効なタイプと互換性のあるタイプ。

—オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン。

—オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型。

—オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型。

—メンバー内に前述のタイプの1つを含む集合体または共用体タイプ(再帰的に、サブ集合体または含まれる共用体のメンバーを含む)、または

—文字タイプ。

そして脚注76は言う:

このリストの目的は、オブジェクトがエイリアスされる場合とされない場合がある状況を指定することです。

C++ドラフト標準 の関連セクションは3.10左辺値と右辺値段落1です。

記事 型のパンニングと厳密なエイリアシング は、トピックのより穏やかですが完全ではない紹介を提供し、 C99の再検討C99との詳細な分析を提供しますエイリアシングであり、軽い読みではありません。 非アクティブな共用体メンバーへのアクセス-未定義? に対するこの回答は、C++の共用体を介した型のパンニングの泥だらけの詳細を調べており、読みやすさもありません。


脚注:

  1. 引用 comment by Pascal Cuoq:[...] C99は最初は不器用な言葉で表現されていたため、ユニオンを介した型のパンニングが未定義になっているようです。実際には、組合はC89で合法であり、C11で合法であり、委員会が誤った表現を修正し、その後TC3をリリースするまでに2004年までかかったが、C99では合法であったが型のパンニング。 open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
41
Shafik Yaghmour

C++では、ポインタ引数は、根本的に異なるタイプを指している場合(char*を除く)、エイリアスしないと見なされます("厳密なエイリアス"ルール)。これにより、いくつかの最適化が可能になります。

ここで、u64 tmpu64として変更されることはありません。
u32*のコンテンツは変更されていますが、 'u64 tmp'とは無関係である可能性があるため、u64 tmpの場合はnopと見なされる場合があります。

47
Jarod42

g ++(Ubuntu/Linaro 4.8.1-10ubuntu9)4.8.1:

> g++ -Wall -std=c++11 -O0 -o sample sample.cpp

> g++ -Wall -std=c++11 -O3 -o sample sample.cpp
sample.cpp: In function ‘uint64_t Swap_64(uint64_t)’:
sample.cpp:10:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(uint32_t*)&tmp)       = Swap_32(*(((uint32_t*)&x)+1));
                   ^
sample.cpp:11:54: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x);
                                                      ^

Clang 3.4は、どの最適化レベルでも警告しません これは興味深いです .. ..

10
pepper_chico