1. ホーム
  2. c++

[解決済み] 動作が未定義のブランチは到達不可能とみなし、デッドコードとして最適化することができますか?

2023-04-03 15:06:38

質問

次の文章を考えてみましょう。

*((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 ステートメント全体が最適化されるかもしれません。このような後方推論は、標準に従って許可されているのでしょうか?

どのように解決するのですか?

あるプログラム中にこのような文があると、プログラム全体が不定になるのでしょうか? プログラム全体が未定義であるということなのでしょうか? 制御フローがこのステートメントにぶつかると、動作が不定になるだけなのでしょうか?

どちらでもありません。最初の条件は強すぎ、2番目は弱すぎです。

オブジェクトのアクセスは時々順番に行われますが、標準は時間以外のプログラムの動作を記述します。Danvilはすでに引用しています。

もしそのような実行が未定義の操作を含んでいるならば、この 国際規格は、その入力でそのプログラムを実行する実装に、何の要件も課しません。 そのプログラムをその入力で実行することに対し、(最初の未定義の操作の前の操作に関しても 最初の未定義の操作の前の操作に関しても)

と解釈することができる。

<ブロッククオート

プログラムの実行が未定義の動作をもたらすのであれば、プログラム全体が未定義の動作を持つことになります。 未定義の振る舞いをすることになります。

つまり、UBで到達不能な文は、プログラムにUBを与えない。入力の値のために)決して到達されない到達可能なステートメントは、プログラムにUBを与えません。そういうわけで、最初の条件は強すぎるのです。

さて、コンパイラは一般的に何がUBを持つかを知ることができません。そのため、オプティマイザーが潜在的な UB を持つステートメントを再順序付けできるようにするには、UB が "reach back in time" と先行するシーケンス ポイントの前に間違って行くことを許可する必要があります (C++11 用語では、UB が UB 事前にシーケンスされたものに影響を与えるためです)。したがって、あなたの 2 番目の条件は弱すぎです。

この主な例は、オプティマイザーが厳密なエイリアシングに依存する場合です。厳密なエイリアシング規則の要点は、問題のポインターが同じメモリをエイリアシングしている可能性がある場合、コンパイラーが有効に再順序付けできない操作を再順序付けできるようにすることです。つまり、不正にエイリアシングされたポインタを使用し、UBが発生した場合、UB文の"before"に簡単に影響を与えることができるのです。抽象的なマシンに関する限り、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 を参照するモードで動作しているためです。 UB であるべきではなく、カーネルが処理できる定義されたハードウェア例外が発生することが期待されています。最適化が有効な場合、gcc では -fno-delete-null-pointer-checks を使用する必要があります。

P.S. 「未定義の動作はいつ起こるのか」という質問に対する実際の答えは、「その日に出発する予定の 10 分前」です。