web-dev-qa-db-ja.com

動作が未定義のブランチは到達不能と見なされ、デッドコードとして最適化されますか?

次のステートメントを検討してください。

*((char*)NULL) = 0; //undefined behavior

明らかに未定義の動作を引き起こします。特定のプログラムにこのようなステートメントが存在するということは、プログラム全体が未定義であること、または制御フローがこのステートメントにヒットしたときにのみ動作が未定義になることを意味しますか?

次のプログラムは、ユーザーが数字3を入力しない場合に備えて明確に定義されていますか?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

それとも、ユーザーが何を入力しても、完全に未定義の動作ですか?

また、コンパイラは、未定義の動作が実行時に実行されないことを想定できますか?これにより、時間を遡って推論することができます。

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

ここで、コンパイラは、num == 3の場合、常に未定義の動作を呼び出すと推論できます。したがって、このケースは不可能でなければならず、番号を印刷する必要はありません。 ifステートメント全体を最適化することができます。この種類の後方推論は規格に従って許可されていますか?

88
usr

特定のプログラムにそのようなステートメントが存在するということは、プログラム全体が未定義であること、または制御フローがこのステートメントにヒットしたときにのみ動作が未定義になることを意味しますか?

どちらでもない。最初の状態は強すぎ、2番目の状態は弱すぎます。

オブジェクトアクセスは時々シーケンスされますが、標準は時間外のプログラムの動作を記述します。 Danvilはすでに引用しています:

そのような実行に未定義の操作が含まれる場合、この国際標準は、その入力でそのプログラムを実行する実装に要件を課しません(最初の未定義の操作の前の操作に関してさえも)

これは解釈できます:

プログラムの実行によって未定義の動作が生じる場合、プログラム全体の動作は未定義です。

したがって、UBで到達できないステートメントはプログラムUBを与えません。 (入力の値のため)到達できないという到達可能なステートメントは、プログラムUBを提供しません。そのため、最初の状態が強すぎます。

現在、コンパイラーは一般にUBの内容を判別できません。したがって、オプティマイザーが、動作が定義された場合に再順序付け可能な潜在的なUBを使用してステートメントを再順序付けできるようにするには、UBが「時間をさかのぼって」前のシーケンスポイント(またはCの前)に失敗することを許可する必要があります++ 11の用語。UBがUBの前に順序付けられるものに影響を与えるため)。したがって、2番目の状態は弱すぎます。

この主な例は、オプティマイザが厳密なエイリアシングに依存している場合です。厳密なエイリアシング規則の要点は、問題のポインターが同じメモリをエイリアスする可能性がある場合に、コンパイラーが有効に並べ替えられなかった操作を並べ替えることを可能にすることです。したがって、不正にエイリアスポインターを使用し、UBが発生した場合、UBステートメントの「前」のステートメントに簡単に影響を与える可能性があります。抽象マシンに関する限り、UBステートメントはまだ実行されていません。実際のオブジェクトコードに関する限り、部分的または完全に実行されています。しかし、標準は、オプティマイザがステートメントを並べ替えることが何を意味するのか、またはそれがUBにどのような影響を与えるのかについては詳しく説明しません。それは、それが喜ばれるとすぐに失敗する実装ライセンスを与えるだけです。

これは「UBにはタイムマシンがある」と考えることができます。

特にあなたの例に答えるために:

  • 3が読み取られた場合の動作は未定義です。
  • 基本ブロックに未定義であることが確実な操作が含まれている場合、コンパイラーはコードをデッドとして除去できます。基本的なブロックではないが、すべてのブランチがUBにつながるケースでは、それらは許可されます(私はそう思います)。この例は、PrintToConsole(3)が確実に戻ることがわかっている場合を除いて、候補にはなりません。例外などをスローする可能性があります。

2番目の例と同様の例は、gccオプション-fdelete-null-pointer-checksです。これは、次のようなコードをとることができます(この特定の例はチェックしていません。一般的なアイデアの例として考えてください)。

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

それを次のように変更します。

*p = 3;
std::cout << "3\n";

どうして? pがnullの場合、コードにはとにかくUBが含まれるため、コンパイラはそれがnullではないと想定し、それに応じて最適化することがあります。 Linuxカーネルは、本質的にこれを逆参照するモードで動作するため、これ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 )をトリップしました。 nullポインターはUBであるはずがないため、カーネルが処理できる定義済みのハードウェア例外が発生することが予想されます。最適化が有効な場合、gccでは、標準を超える保証を提供するために-fno-delete-null-pointer-checksを使用する必要があります。

追伸「未定義の行動はいつ起こるか」という質問への実際的な答え。 「その日に出発する予定の10分前」です。

63
Steve Jessop

標準状態は1.9/4です。

[注:この国際規格は、未定義の動作を含むプログラムの動作に要件を課していません。 —エンドノート]

興味深い点は、おそらく「含む」が意味することです。少し後に1.9/5でそれは述べています:

ただし、そのような実行に未定義の操作が含まれている場合、この国際標準は、その入力でそのプログラムを実行する実装に要件を課しません(最初の未定義の操作の前の操作に関してさえも)。

ここでは、特に「その入力での実行...」に言及しています。現在実行されていない可能性のある1つの分岐での未定義の動作は、現在の実行分岐には影響を与えないと解釈します。

ただし、別の問題は、コード生成中の未定義の動作に基づく仮定です。詳細については、Steve Jessopの回答を参照してください。

10
Danvil

有益な例は

_int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}
_

現在のGCCと現在のClangの両方がこれを(x86で)最適化して

_xorl %eax,%eax
ret
_

それらはxが常にゼロであることを推定するif (x)制御パスのUBからです。 GCCは、使用されていない初期化された値の警告さえ提供しません! (上記のロジックを適用するパスは、初期化されていない値の警告を生成するパスの前に実行されるため)

5
zwol

現在のC++ワーキングドラフトは1.9.4で

この国際規格は、未定義の動作を含むプログラムの動作に要件を課していません。

これに基づいて、実行パスに未定義の動作を含むプログラムは、その実行のたびに何でも実行できると思います。

未定義の動作とコンパイラーが通常行うことについて、2つの本当に良い記事があります。

4
Jens

未定義の動作は、次に何が起こってもプログラムが未定義の動作を引き起こす場合に発生します。ただし、次の例を示しました。

_int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}
_

コンパイラがPrintToConsoleの定義を知らない限り、if (num == 3)条件付きを削除できません。 _LongAndCamelCaseStdio.h_システムヘッダーがあり、次のPrintToConsoleが宣言されているとします。

_void PrintToConsole(int);
_

あまり役に立ちませんでした。ここで、この関数の実際の定義をチェックして、ベンダーがどれほど悪(またはそれほど悪ではなく、未定義の動作が悪化していたかもしれない)かを見てみましょう。

_int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}
_

コンパイラーは実際には、コンパイラーが何を行うかを知らない任意の関数が終了するか、例外をスローする可能性があると想定する必要があります(C++の場合)。 PrintToConsoleを呼び出した後は実行が継続されないため、*((char*)NULL) = 0;は実行されないことに気づくでしょう。

PrintToConsoleが実際に戻ると、未定義の動作が発生します。コンパイラはこれが発生しないことを期待しているため(これが原因でプログラムが未定義の動作を実行するため)、何かが発生する可能性があります。

ただし、別のことを考えてみましょう。 nullチェックを実行していて、nullチェック後に変数を使用するとします。

_int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}
_

この場合、_lol_null_check_にはNULL以外のポインタが必要であることが簡単にわかります。グローバルな非揮発性warning変数への割り当ては、プログラムを終了したり、例外をスローしたりすることはできません。 pointerも不揮発性であるため、関数の途中で値を魔法のように変更することはできません(変更する場合は、未定義の動作です)。 lol_null_check(NULL)を呼び出すと、未定義の動作が発生し、変数が割り当てられない可能性があります(この時点では、プログラムが未定義の動作を実行することがわかっているため)。

ただし、未定義の動作は、プログラムが何でも実行できることを意味します。したがって、未定義の動作が過去に戻って、int main()の最初の行が実行される前にプログラムがクラッシュすることを止めるものはありません。これは未定義の動作であり、意味をなす必要はありません。 3を入力した後もクラッシュする可能性がありますが、未定義の動作は過去に戻り、3を入力する前にクラッシュします。そして、おそらく未定義の動作がシステムRAMを上書きし、2週間後にシステムをクラッシュさせるでしょう。未定義のプログラムが実行されていない間。

3
Konrad Borowski

「振る舞い」という言葉は、何かが起こっていることを意味します完了。決して実行されないstatemenrは「ふるまい」ではありません。

イラスト:

*ptr = 0;

それは未定義の動作ですか? 100%確実であるとしましょうptr == nullptrプログラム実行中に少なくとも1回。答えはイエスです。

これはどうですか?

 if (ptr) *ptr = 0;

未定義ですか? (ptr == nullptr少なくとも1回ですか?)そうでないことを望みます。そうしないと、有用なプログラムをまったく作成できなくなります。

この回答の作成に害を及ぼすスランダルデスはありません。

3

プログラムが未定義の動作を呼び出すステートメントに到達した場合、プログラムの出力/動作のいずれにも要件は課されません。未定義の動作が呼び出される「前」または「後」のどちらで行われるかは関係ありません。

3つのコードスニペットすべてについてのあなたの推論は正しいです。特に、コンパイラーは、GCCが__builtin_unreachable()を処理する方法で未定義の動作を無条件に呼び出すすべてのステートメントを、ステートメントに到達できないという最適化のヒントとして扱う可能性があります(これにより、ステートメントに無条件につながるすべてのコードパスも到達不能になる)。もちろん、他の同様の最適化も可能です。

IETF RFC 2119 で定義されているものと同様の命名法を使用して、実装が行うべきまたはすべきでないことを説明することに多くの種類の多くの標準が多大な労力を費やします(ただし、必ずしもそのドキュメントの定義を引用しているわけではありません) )。多くの場合、実装が行うべきことの説明が役に立たないか、実用的でない場合は、all準拠する実装は準拠する必要があります。

残念ながら、CおよびC++標準は、100%必須ではないものの、逆の動作を文書化しない高品質の実装に期待されるものの説明を避ける傾向があります。実装が何かを行うべきであるという提案は、劣っていないものを暗示するものと見なされる可能性があり、特定の実装で、どの動作が実用的で実用的ではなく、実際的で役に立たないかが一般的に明らかである場合、基準がそのような判断に干渉する必要性はほとんど認識されていません。

賢いコンパイラーは標準に準拠しながら、コードが未定義の動作を必然的に引き起こす入力を受け取った場合を除いて何の影響もないコードを排除しますが、「賢い」と「ダム」は反義語ではありません。規格の作成者が、特定の状況で有効に動作することが役に立たず実用的ではない実装の種類があるかもしれないと決定したという事実は、そのような動作が他の動作で実用的で有用であると見なされるべきかどうかの判断を意味しません。実装が「デッドブランチ」プルーニング機会の損失を超えてコストなしで動作保証を維持できる場合、ユーザーコードがその保証から受け取るほぼすべての価値は、それを提供するコストを超えることになります。 何かをあきらめる必要がない場合は、デッドブランチの除去で問題ないかもしれませんが、特定の状況でユーザーコードがほとんどすべてを処理できた場合可能な動作デッドブランチの除去以外、UBEがDBEから得られる値を超える可能性を回避するためにユーザーコードが費やさなければならないあらゆる努力。

1
supercat