デッドコードがコンパイラによって検出できないことの証明


32

私はさまざまなトピックの冬のコースを教えることを計画しています。そのうちの1つはコンパイラーになります。今、私はこの問題に出くわしましたが、四半期を通じて与えるべき課題を考えていましたが、私は困惑しているので、代わりに例としてそれを使用するかもしれません。

public class DeadCode {
  public static void main(String[] args) {
     return;
     System.out.println("This line won't print.");
  }
}

上記のプログラムでは、のためにprintステートメントが実行されないことは明らかですreturn。コンパイラーは、デッドコードに関する警告またはエラーを出すことがあります。たとえば、上記のコードはJavaでコンパイルされません。ただし、javacコンパイラは、すべてのプログラムでデッドコードのすべてのインスタンスを検出するわけではありません。コンパイラーがそれを実行できないことをどのように証明しますか?


29
あなたのバックグラウンドは何ですか?また、あなたが教えているコンテキストは何ですか?率直に言って、私はあなたが教えようとしているのを見て、あなたがこれを尋ねなければならないことを少し心配しています。しかし、ここで尋ねるのは良い電話です!
ラファエル


9
@MichaelKjörlingデッドコードの検出は、これらの考慮事項がなくても不可能です。
デビッドリチャービー

2
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
user253751

2
@immibisこの質問は、デッドコードの検出が不可能であることの証明を求めています。デッドコードを正しく検出するには、数学の未解決の問題を解決する必要がある例を示しました。それは、デッドコード検出が不可能であること証明しません。
デヴィッドリチャービー

回答:


57

それはすべて、停止する問題の決定不能から来ています。「完全な」デッドコード関数、Turing Machine M、入力文字列x、および次のようなプロシージャがあるとします。

Run M on input x;
print "Finished running input";

Mが永遠に実行される場合、printステートメントは削除されます。Mが永久に実行されない場合、printステートメントを保持する必要があります。したがって、デッドコードリムーバーがあれば、ホールティング問題も解決できるので、そのようなデッドコードリムーバーはありえないことがわかります。

これを回避する方法は、「保守的な近似」です。したがって、上記のチューリングマシンの例では、xでのMの実行が終了する可能性があると想定できるため、安全に再生し、printステートメントを削除しません。あなたの例では、どの関数が停止しても停止しなくても、そのprintステートメントに到達する方法がないことを知っています。

通常、これは「制御フローグラフ」を作成することによって行われます。「whileループの終わりは最初とその後のステートメントに接続されている」などの単純化された仮定を作成します。同様に、実際には一部が使用されない場合でも、ifステートメントはすべてのブランチに到達できると想定しています。これらの種類の単純化により、決定可能なままで、例のように「明らかにデッドコード」を削除できます。

コメントからいくつかの混乱を明確にするには:

  1. Nitpick:固定Mの場合、これは常に決定可能です。Mは入力である必要があります

    Raphaelが言うように、私の例では、チューリングマシンを入力として考えます。アイデアは、完全なDCEアルゴリズムがあれば、チューリングマシンに指定したコードスニペットを構築でき、DCEがあれば停止の問題を解決できるということです。

  2. 納得できません。分岐なしの単純な実行での鈍い文として戻ることは、決定するのが難しくありません。(そして、私のコンパイラはこれを理解することができると私に言っています)

    njzk2が発生する問題については、あなたは絶対に正しいです。この場合、戻り値に到達した後にステートメントが到達する方法がないと判断できます。これは、制御フローグラフの制約を使用してその到達不能性を説明できるほど単純であるためです(つまり、returnステートメントから外へ出るエッジはありません)。ただし、完全なデッドコードエリミネーターはなく、未使用のコードをすべて排除します。

  3. 証明のために入力依存の証明を取りません。コードを有限にすることができるような種類のユーザー入力が存在する場合、コンパイラーは後続のブランチがデッドではないと想定するのが正しいです。私はこれらすべての賛成票が何のためにあるか見ることができません、それは明らかです(例えば、無限の標準入力)そして間違っています。

    TomášZatoの場合:これは実際には入力依存の証明ではありません。むしろ、「forall」として解釈してください。次のように機能します。完全なDCEアルゴリズムがあると仮定します。任意のチューリングマシンMと入力xを渡すと、DCEアルゴリズムを使用して、上記のコードスニペットを作成し、print-statementが削除されるかどうかを確認することにより、Mが停止するかどうかを判断できます。パラメータを任意に残してforallステートメントを証明するこの手法は、数学や論理では一般的です。

    コードが有限であるというトマシュザトの主張を完全には理解していません。確かにコードは有限ですが、完全なDCEアルゴリズムはすべてのコードに適用する必要があります。これは無限のセットです。同様に、コード自体は有限ですが、入力の潜在的なセットは無限であり、コードの潜在的な実行時間も同様です。

    デッドではない最終ブランチの検討に関しては、「保守的な近似」の観点からは安全ですが、OPが求めるようにデッドコードのすべてのインスタンスを検出するだけでは不十分です。

次のようなコードを検討してください。

while (true)
  print "Hello"
print "goodbye"

明らかprint "goodbye"に、プログラムの動作を変更せずに削除できます。したがって、それはデッドコードです。ただし(true)while条件内ではなく別の関数呼び出しがある場合、それを削除できるかどうかわからないため、決定できません。

私は自分でこれを考え出していないことに注意してください。これは、コンパイラの理論でよく知られている結果です。The Tiger Bookで議論されています。(あなたは彼らがGoogleブックのどこで話しているかを見ることができるかもしれません。


1
@ njzk2:私たちは、それはデッドコードの除去を構築することは不可能です示そうとしていることを排除全てデッドコード、それはデッドコードの除去なくすこと構築することは不可能だということではない、いくつかのデッドコードを。リターンアフタープリントの例は、制御フローグラフテクニックを使用して簡単に削除できますが、この方法ですべてのデッドコードを削除できるわけではありません。
user2357112は

4
この回答はコメントを参照しています。答えを読んで、コメントに飛び込んでから答えに戻る必要があります。これは混乱を招きます(コメントが壊れやすく、失われる可能性があると考えると、疑わしいでしょう)。自己完結型の答えは、はるかに読みやすくなります。
TRiG

1
@TomášZato-変数をインクリメントし、が奇数の完全数であるかどうかをチェックし、そのような数が見つかった場合にのみ終了するプログラムを検討します。明らかに、このプログラムは外部入力に依存しません。このプログラムが終了するかどうかを簡単に判断できると断言していますか?nn
グレゴリーJ.プレオ

3
MバツMバツ

1
jmite、有効なコメントを回答に組み込んで、回答が自立するようにしてください。次に、廃止されたすべてのコメントにフラグを立てて、クリーンアップできるようにします。ありがとう!
ラファエル

14

これは、非終了に関する潜在的な混乱を回避するjmiteの答えのひねりです。常にそれ自体を停止し、デッドコードが存在する可能性があるプログラムを提供しますが、それが存在するかどうかを(常に)アルゴリズム的に決定することはできません。

デッドコード識別子の入力の次のクラスを検討してください。

simulateMx(n) {
  simulate TM M on input x for n steps
  if M did halt
    return 0
  else
    return 1
}

Mxが修正されているため、で停止しない場合に限り、simulateMsデッドコードがreturn 0あります。Mx

MxMM

したがって、デッドコードチェックは計算できません。

この文脈で証明手法としての縮小に慣れていない場合は、参考資料をお勧めします


5

詳細に行き詰まることなく、この種のプロパティを示す簡単な方法は、次の補題を使用することです。

補題:チューリング完全言語用のコンパイラCには、undecidable_but_true()引数をとらずブール値trueを返す関数が存在するため、C undecidable_but_true()はtrueを返すかfalseを返すかを予測できません。

関数はコンパイラに依存することに注意してください。関数を指定するundecidable_but_true1()と、コンパイラは常に、この関数がtrueまたはfalseを返すかどうかの知識で拡張できます。しかし、undecidable_but_true2()カバーされない他の機能が常にあります。

証明:ライスの定理、プロパティ「真、この関数が返す」決定不能です。したがって、静的解析アルゴリズムは、考えられるすべての機能についてこのプロパティを決定できません。

結果:コンパイラCを指定すると、次のプログラムには検出できないデッドコードが含まれています。

if (!undecidable_but_true()) {
    do_stuff();
}

Javaに関する注意:Java言語では、コンパイラーは到達不能コードを含む特定のプログラムを拒否することを義務付けていますが、そのコードは到達可能なすべてのポイントで提供されることを賢明に義務付けています(たとえば、非void関数の制御フローはreturnステートメントで終了する必要があります)。言語は、到達不能コード分析の実行方法を正確に指定します。そうでなければ、移植可能なプログラムを書くことは不可能でしょう。次の形式のプログラムを考える

some_method () {
    <code whose continuation is unreachable>
    // is throw InternalError() needed here?
}

どの場合に到達不能コードの後に​​他のコードが続かなければならず、どの場合にコードが続かないかを指定する必要があります。到達不能なコードを含むJavaプログラムの例は、Javaコンパイラーが認識できる方法ではないが、Java 101で登場します。

String day_of_week(int n) {
    switch (n % 7) {
    case 0: return "Sunday";
    case 1: case -6: return "Monday";
    …
    case 6: case -1: return "Saturday";
    }
    // return or throw is required here, even though this point is unreachable
}

一部の言語の一部のコンパイラは、末尾にday_of_week到達できないことを検出できる場合があることに注意してください。
user253751

@immibisはい、たとえば、CS101の学生は私の経験でそれを行うことができます(確かにCS101の学生は健全な静的アナライザーではありませんが、ネガティブなケースは通常忘れます)。それは私のポイントの一部です:それは、Javaコンパイラーが検出できない(少なくとも警告を発するかもしれませんが、拒否しないかもしれない)到達不能なコードを持つプログラムの例です。
ジル「SO-悪であるのをやめる」

1
補題のフレージングは​​せいぜい誤解を招くだけで、それに多少の誤りがあると思います。決定不能性は、インスタンスの(無限の)セットの用語で表現する場合にのみ意味があります。(コンパイラーすべての関数に対して答えを生成しますが、常に正しいとは限りませんが、単一の未決定のインスタンスがあると言っています。)補題と証明の間の段落(補題に完全には一致しません述べたように)これを修正しようとしますが、明確に正しい補題を定式化する方が良いと思います。
ラファエル

@Raphael Uh?いいえ、コンパイラは「この関数は定数ですか?」という質問に答える必要はありません。動作するコードを生成するために「わからない」と「いいえ」を区別する必要はありませんが、コード変換部分ではなく、コンパイラの静的分析部分のみに関心があるため、ここでは関係ありません。あなたが補題の記述について誤解を招くまたは間違っていると思うものを理解していません—あなたのポイントが「コンパイラー」の代わりに「静的アナライザー」を書くべきであるということでない限り?
ジル 'SO-悪であるのをやめる'

この文は、「決定不能性とは、解決できないインスタンスがあることを意味する」と思われますが、これは誤りです。(私はあなたがそれを言うつもりはないことを知っています、しかしそれはそれが不注意な/初心者のために読むことができる方法です、私見。)
ラファエル

3

jmiteの答えは、プログラムが計算を終了するかどうかに適用されます。それが無限だからといって、コードを呼び出した後は呼び出したくないからです。

ただし、別のアプローチがあります。答えはあるが不明な問題です。

public void Demo()
{
  if (Chess.Evaluate(new Chessboard(), int.MaxValue) != 0)
    MessageBox.Show("Chess is unfair!");
  else
    MessageBox.Show("Chess is fair!");
}

public class chess
{
  public Int64 Evaluate(Chessboard Board, int SearchDepth)
  {
  ...
  }
}

間違いなくこのルーチンにはデッドコード含まれてます。この関数は、1つのパスを実行し、他のパスは実行しないという応答を返します。しかし、それを見つけて頑張ってください!私の記憶は、宇宙の寿命内でこれを解決できる理論的なコンピューターではありません。

さらに詳細に:

このEvaluate()関数は、両方の側が完全に(最大の検索深度で)プレイする場合、どちらの側がチェスゲームに勝つかを計算します。

チェスエバリュエーターは通常、指定された深さのあらゆる可能な動きを先読みし、その時点でボードにスコアを付けようとします(交換などの途中で見ると特定のブランチをさらに広げると、非常にゆがんだ認識が得られることがあります)。 17695半動きで検索は網羅的で、可能なチェスゲームをすべて横断します。すべてのゲームが終了するため、各ボードの位置がどれだけ良いかを判断しようとする問題はないため(したがって、ボード評価ロジックを参照する必要はありません-呼び出されることはありません)、結果は勝ち、負け、または引き分け。結果が引き分けの場合、ゲームは公平です。結果が引き分けでない場合、それは不公平なゲームです。少し拡張すると、次のようになります。

public Int64 Evaluate(Chessboard Board, int SearchDepth)
{
  foreach (ChessMove Move in Board.GetPossibleMoves())
    {
      Chessboard NewBoard = Board.MakeMove(Move);
      if (NewBoard.Checkmate()) return int.MaxValue;
      if (NewBoard.Draw()) return 0;
      if (SearchDepth == 0) return NewBoard.Score();
      return -Evaluate(NewBoard, SearchDepth - 1);
    }
}

また、コンパイラがChessboard.Score()がデッドコードであることを認識することは事実上不可能であることにも注意してください。チェスのルールを知っていれば、人間はこれを理解できますが、これを理解するには、MakeMoveがピースカウントを増やすことはできず、ピースカウントがあまりにも長く静的のままである場合、Chessboard.Draw()がtrueを返すことを知っておく必要があります。

検索の深さは、全体の動きではなく、半分の動きであることに注意してください。これはO(x ^ n)ルーチンであるため、この種のAIルーチンでは正常です。もう1つの検索プライを追加すると、実行にかかる時間に大きな影響があります。


8
チェックアルゴリズムが計算を実行する必要があると仮定します。よくある誤解!いいえ、チェッカーがどのように機能するについては何も仮定しません。そうでなければ、その存在に反論することはできません。
ラファエル

6
質問は、デッドコードを検出することが不可能であることの証明を要求します。あなたの投稿は、あなたがた場合の例が含ま疑い、次のようになり難しいデッドコードを検出することを。それは目前の質問に対する答えではありません。
デビッドリチャービー

2
@LorenPechtel私は知りませんが、それは証拠ではありません。こちらもご覧ください。あなたの誤解のよりきれいな例。
ラファエル

3
それが役立つ場合は、理論上、誰かが宇宙の寿命を超えてコンパイラーを実行するのを妨げるものは何もないと考えてください。唯一の制限は実用性です。決定可能な問題は、複雑度クラスNONELEMENTARYにある場合でも、決定可能な問題です。
仮名

4
言い換えれば、この答えはせいぜい、すべてのデッドコードを検出するコンパイラを構築するのがおそらく簡単ではない理由を示すことを目的としたヒューリスティックですが、不可能ではないという証拠ではありません。この種の例、学生の直感を構築する方法として有用かもしれませんが、それは証拠ではありません。自身を証拠として提示することにより、それは損害を与えます。答えは、直観を構築する例であり、不可能性の証拠ではないことを述べるように編集する必要があります。
DW

-3

コンピューティングコースでは、デッドコードの概念は、コンパイル時と実行時の違いを理解するという点で興味深いと思います!

コンパイラは、コンパイル時のシナリオでは走査できないコードを取得したかどうかを判断できますが、ランタイムでは実行できません。ループブレークテスト用のユーザー入力を備えた単純なwhileループがそれを示しています。

コンパイラが実際にランタイムデッドコードを判断できる場合(つまり、チューリングの完了を識別する場合)、ジョブは既に実行されているため、コードを実行する必要がないという議論があります。

コンパイル時のデッドコードチェックに合格するコードの存在は、入力および一般的なコーディング衛生(実際のプロジェクトの現実の世界)での実用的な境界チェックの必要性を示しています。


1
質問は、デッドコードを検出することが不可能であるという証拠を求めています。あなたはその質問に答えていません。
デビッドリチャービー

また、「コンパイル時のシナリオでは通過できないコードをいつコンパイラが決定できるか」という主張は誤りであり、質問が証明するように求めていることと直接矛盾しています。
デビッドリチャービー

@David Richerby、あなたは私を誤解していると思う。コンパイル時のチェックですべてのデッドコードを検出できることを提案しているわけではありません。コンパイル時に認識できるすべてのデッドコードのセットのサブセットがあることをお勧めします。if(true == false){print( "something");}と書くと、そのprintステートメントはコンパイル時に認識できなくなり、デッドコードになります。これがあなたの主張に対する反例であることに同意しませんか?
dwoz

確かに、いくつかのデッドコードを特定できます。しかし、「[デッドコードがあるとき]を決定する」という条件を付けずに言う場合、それは、私にとっては、一部だけでなく、すべてのデッドコードを見つけることを意味します。
デビッドリチャービー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.