コンパイラーでデッドコード検出を完全に解決できないのはなぜですか?


192

私がCまたはJavaで使用していたコンパイラーには、デッドコード防止機能(行が実行されない場合に警告)があります。私の教授は、この問題はコンパイラーでは完全には解決できないと言います。なぜなのかと思っていました。これは理論に基づくクラスなので、コンパイラの実際のコーディングについてはあまり詳しくありません。しかし、私は彼らが何をチェックするのか(考えられる入力文字列と許容可能な入力など)、なぜそれが不十分なのかと思っていました。


91
ループを作成し、その後にコードを配置してから、en.wikipedia.org
wiki / Halting_problemを

48
if (isPrime(1234234234332232323423)){callSomething();}このコードは何かを呼び出すかどうか。他にも多くの例があり、関数が呼び出されるかどうかを決定することは、単にプログラムに関数を含めるよりもはるかにコストがかかります。
idclev 463035818

33
public static void main(String[] args) {int counterexample = findCollatzConjectureCounterexample(); System.out.println(counterexample);}<-println呼び出しはデッドコードですか?人間でさえそれを解決することはできません!
user253751

15
@ tobi303は良い例ではありません。素数を因数分解するのは本当に簡単です...比較的効率的に因数分解しないだけです。停止の問題はNPではなく、解決できません。
en_Knight、2015年

57
@alephzeroとen_Knight-あなたはどちらも間違っています。isPrimeは良い例です。関数が素数をチェックしていると仮定しました。たぶんその番号はシリアル番号でしたが、データベースルックアップを実行して、ユーザーがAmazon Primeメンバーであるかどうかを確認していますか?これが素晴らしい例である理由は、条件が一定であるかどうかを知る唯一の方法は、実際にisPrime関数を実行することだからです。そのため、コンパイラーもインタープリターである必要があります。しかし、それでもデータが不安定な場合は解決されません。
2015年

回答:


275

デッドコードの問題は、停止の問題に関連しています

アラン・チューリングは、プログラムを与えられ、そのプログラムがすべての入力で停止するかどうかを決定できる一般的なアルゴリズムを書くことは不可能であることを証明しました。特定のタイプのプログラムに対してこのようなアルゴリズムを作成できる場合がありますが、すべてのプログラムについてはそうではありません。

これはデッドコードとどのように関係していますか?

停止の問題は、死んだコードを見つける問題に還元できます。つまり、任意のプログラムでデッドコードを検出できるアルゴリズムを見つけた場合、そのアルゴリズムを使用してプログラムが停止するかどうかをテストできます。それは不可能であることが証明されているので、デッドコードのアルゴリズムを書くことも不可能であるということになります。

デッドコードのアルゴリズムを停止問題のアルゴリズムにどのように転送しますか?

単純:停止を確認するプログラムの終了後にコード行を追加します。デッドコード検出器がこの行がデッドであることを検出した場合、プログラムは停止しないことがわかります。そうでない場合は、プログラムが停止していることがわかります(最後の行に移動してから、追加したコード行に移動します)。


コンパイラは通常、コンパイル時に死んでいることが証明できるものがないかチェックします。たとえば、コンパイル時にfalseであると判断できる条件に依存するブロック。または、aの後のステートメントreturn(同じスコープ内)。

これらは特定のケースであるため、それらのアルゴリズムを作成することが可能です。もっと複雑なケース(条件が構文的に矛盾しているかどうかをチェックし、そのため常にfalseを返すかどうかをチェックするアルゴリズムなど)のアルゴリズムを作成することは可能かもしれませんが、それでもすべての可能なケースをカバーするわけではありません。


8
現実世界のすべてのコンパイラーのコンパイルターゲットであるすべてのプラットフォームには、アクセスできるデータの最大量があるため、停止問題はここでは適用できないと主張します。したがって、最大数の状態があり、実際、チューリングマシンではなく有限状態マシンです。停止の問題はFSMにとって解決不可能なものではないため、現実世界のどのコンパイラでもデッドコード検出を実行できます。
Vality

50
@Vality 64ビットプロセッサは2 ^ 64バイトをアドレス指定できます。すべての256 ^(2 ^ 64)州を検索して楽しんでください!
Daniel Wagner、

82
@DanielWagnerこれは問題になりません。256^(2^64)状態の検索はO(1)なので、デッドコード検出は多項式時間で実行できます。
aebabis 2015年

13
@Leliel、それは皮肉でした。
Paul Draper、

44
@Vality:最近のほとんどのコンピューターには、ディスク、入力デバイス、ネットワーク通信などがあります。完全な分析では、文字通り、インターネットとそれに接続されているすべてのものを含め、そのようなすべてのデバイスを考慮する必要があります。これは扱いにくい問題ではありません。
2015年

77

さて、停止問題の決定不可能性の古典的な証明を取り、停止検出器をデッドコード検出器に変更しましょう!

C#プログラム

using System;
using YourVendor.Compiler;

class Program
{
    static void Main(string[] args)
    {
        string quine_text = @"using System;
using YourVendor.Compiler;

class Program
{{
    static void Main(string[] args)
    {{
        string quine_text = @{0}{1}{0};
        quine_text = string.Format(quine_text, (char)34, quine_text);

        if (YourVendor.Compiler.HasDeadCode(quine_text))
        {{
            System.Console.WriteLine({0}Dead code!{0});
        }}
    }}
}}";
        quine_text = string.Format(quine_text, (char)34, quine_text);

        if (YourVendor.Compiler.HasDeadCode(quine_text))
        {
            System.Console.WriteLine("Dead code!");
        }
    }
}

YourVendor.Compiler.HasDeadCode(quine_text)返された場合false、その行System.Console.WriteLn("Dead code!");は実行されないため、このプログラムに実際にデッドコードがあり、検出器が間違っていました。

しかし、それがを返したtrue場合、その行System.Console.WriteLn("Dead code!");が実行され、プログラム内にコードがなくなるため、デッドコードがまったくなくなり、検出器が間違っていました。

つまり、「デッドコードあり」または「デッドコードなし」のみを返すデッドコード検出器は、間違った答えを返すことがあります。


1
私があなたの議論を正しく理解していれば、技術的に別のオプションは、デッドコード検出器であるquitを書くことができないということですが、一般的なケースではデッドコード検出器を書くことが可能です。:-)
2015年

1
Godelian回答の増分。
Jared Smith

@ablighああ、それは言葉の悪い選択でした。私は実際にデッドコード検出器のソースコードをそれ自体に供給しているのではなく、それを使用するプログラムのソースコードを供給しています。確かに、ある時点で、それはおそらくそれ自身のコードを見なければならないでしょうが、それはそのビジネスです。
Joker_vD

65

停止の問題があまりにもあいまいな場合は、このように考えてください。

すべての正の整数のために真であると考えられている数学の問題乗りのnが、すべてのために真であることが証明されていないn個を。良い例はゴールドバッハの予想です。2より大きい正の偶数は2つの素数の和で表すことができます。次に、(適切なbigintライブラリを使用して)このプログラムを実行します(擬似コードが続きます)。

 for (BigInt n = 4; ; n+=2) {
     if (!isGoldbachsConjectureTrueFor(n)) {
         print("Conjecture is false for at least one value of n\n");
         exit(0);
     }
 }

の実装はisGoldbachsConjectureTrueFor()読者の練習問題として残されていますが、この目的のために、すべての素数よりも単純な反復である可能性がありますn

さて、論理的には上記は以下と同等でなければなりません:

 for (; ;) {
 }

(つまり、無限ループ)または

print("Conjecture is false for at least one value of n\n");

ゴールドバッハの予想が真実かそうでないかのどちらかであるように。コンパイラーが常にデッドコードを排除できるのであれば、どちらの場合も確実にここで排除するデッドコードがあります。ただし、そうする場合、少なくともコンパイラは任意の難しい問題を解決する必要があります。どのビットのコードを除去するかを決定するために解決しなければならないであろう証明困難な問題(たとえば、NP完全な問題)を提供することができます。たとえば、次のプログラムを実行するとします。

 String target = "f3c5ac5a63d50099f3b5147cabbbd81e89211513a92e3dcd2565d8c7d302ba9c";
 for (BigInt n = 0; n < 2**2048; n++) {
     String s = n.toString();
     if (sha256(s).equals(target)) {
         print("Found SHA value\n");
         exit(0);
     }
 }
 print("Not found SHA value\n");

プログラムは「Found SHA value」または「Not found SHA value」のいずれかを出力することを知っています(どちらが真であるかを教えてくれればボーナスポイント)。ただし、コンパイラーが合理的に最適化できるようにするには、2 ^ 2048回の繰り返しが必要になります。上記のプログラムは最適化せずに何かを印刷するのではなく、宇宙の熱死まで実行される(または実行される可能性がある)と私は予測しているので、それは実際には素晴らしい最適化です。


4
これは、はるかに+1のベストアンサーです
ジャン

2
特に興味深いのは、ループが終了すると想定する場合に、C標準が許可するものと許可しないもののあいまいさです。コンパイラが結果を実際に必要とするポイントまで結果を使用する場合と使用しない場合がある遅い計算を延期できるようにすることには価値があります。この最適化は、コンパイラが計算の終了を証明できない場合でも、場合によっては役立つことがあります。
スーパーキャット2015年

2
2 ^ 2048回の反復?深い思考でさえあきらめるでしょう。
Peter Mortensen

そのターゲットが64桁の16進数のランダム文字列であったとしても、非常に高い確率で「Found SHA value」を出力します。sha256バイト配列を返さない限り、バイト配列は、使用している言語の文字列と比較されません。
user253751 2015年

4
Implementation of isGoldbachsConjectureTrueFor() is left as an exercise for the readerこれは私を笑わせました。
biziclop 2015年

34

C ++またはJavaにEval型関数があるかどうかはわかりませんが、多くの言語ではメソッドを名前で呼び出すことができます。次の(考案された)VBAの例を考えます。

Dim methodName As String

If foo Then
    methodName = "Bar"
Else
    methodName = "Qux"
End If

Application.Run(methodName)

呼び出されるメソッドの名前は、実行時まで知ることができません。したがって、コンパイラーは定義上、特定のメソッドが呼び出されないことを確実に知ることはできません。

実際、名前でメソッドを呼び出す例を考えると、分岐ロジックは必要ありません。単に言って

Application.Run("Bar")

コンパイラが判断できる以上のものです。コードがコンパイルされるとき、コンパイラはすべて、特定の文字列値がそのメソッドに渡されていることを知っています。このメソッドが存在するかどうかは、実行時まで確認されません。メソッドが他の場所で呼び出されない場合、より通常のメソッドを介して、デッドメソッドを見つけようとすると、誤検知が返される可能性があります。同じ問題が、リフレクションを介してコードを呼び出すことができるすべての言語に存在します。


2
Java(またはC#)では、これはリフレクションで実行できます。C ++では、おそらくそれを行うためにマクロを使用して、いくつかの厄介さを取り除くことができます。美しくありませんが、C ++はめったにありません。
Darrel Hoffman

6
@DarrelHoffman-マクロはコードがコンパイラーに渡される前に展開されるため、マクロはこれを行う方法とは異なります。関数へのポインターは、これを行う方法です。私は何年もC ++を使用していないので、正確な型名が間違っている場合は失礼しますが、関数ポインタへの文字列のマップを格納するだけで済みます。次に、ユーザー入力から文字列を受け取り、マップでその文字列を検索し、ポイントされた関数を実行するものを用意します。
ArtOfWarfare 2015年

1
@ArtOfWarfareは、それがどのように行われるかについては話していません。明らかに、コードのセマンティック分析を実行してこの状況を見つけることができます。要点は、コンパイラーがそうではないということでした。可能性はあるかもしれませんが、そうではありません。
RubberDuck、2015年

3
@ArtOfWarfare:ひっくり返す場合は、確認してください。プリプロセッサーはコンパイラーの一部と見なしていますが、技術的にはそうではありません。とにかく、関数ポインターは、関数がどこでも直接参照されないという規則に違反する可能性があります。これらは、直接呼び出しではなくポインターとして、C#のデリゲートによく似ています。C ++は間接的に物事を行う方法が非常に多いため、一般にコンパイラーが予測するのははるかに困難です。「すべての参照を見つける」という単純なタスクでさえ、typedefやマクロなどで非表示になる可能性があるため、簡単ではありません。デッドコードを簡単に見つけられないのも当然です。
Darrel Hoffman、

1
この問題に直面するために、動的なメソッド呼び出しも必要ありません。パブリックメソッドは、JavaまたはC#で既にコンパイルされているクラス、または動的リンク用のメカニズムを備えた他のコンパイル済み言語に依存する、まだ記述されていない関数によって呼び出すことができます。コンパイラがこれらを「デッドコード」として排除した場合、配布用にプリコンパイルされたライブラリ(NuGet、jar、バイナリコンポーネントを含むPythonホイール)をパッケージ化できなくなります。
jpmc26 2015年

12

無条件のデッドコードは、高度なコンパイラによって検出および削除できます。

しかし、条件付きデッドコードもあります。これは、コンパイル時に認識できず、実行時にのみ検出できるコードです。たとえば、ソフトウェアは、ユーザーの好みに応じて特定の機能を含めたり除外したりするように構成でき、特定のシナリオではコードの特定のセクションが死んでいるように見えます。それは本当のデッドコードではありません。

テストを実行し、依存関係を解決し、条件付きデッドコードを削除し、実行時に効率的なコードを再結合できる特定のツールがあります。これは動的デッドコード除去と呼ばれます。しかし、ご覧のとおり、コンパイラの範囲を超えています。


5
「無条件のデッドコードは、高度なコンパイラによって検出および削除できます。」これはありそうにありません。コードのデッドネスは特定の関数の結果に依存する可能性があり、その特定の関数は任意の問題を解決できます。したがって、ステートメントは、高度なコンパイラが任意の問題を解決できると主張します。
Taemyr

6
@Taemyrそれなら、無条件に死んだとは知られていないでしょうが、今はそうでしょうか?
JAB

1
@Taemyr「無条件」という言葉を誤解しているようです。コードのデッドネスが関数の結果に依存する場合、それは条件付きデッドコードです。「状態」は、関数の結果です。「無条件」であるためには、結果に依存する必要はありません
キョーティック2015年

12

簡単な例:

int readValueFromPort(const unsigned int portNum);

int x = readValueFromPort(0x100); // just an example, nothing meaningful
if (x < 2)
{
    std::cout << "Hey! X < 2" << std::endl;
}
else
{
    std::cout << "X is too big!" << std::endl;
}

ここで、ポート0x100が0または1のみを返すように設計されていると想定します。その場合、コンパイラーはelseブロックが実行されないことを理解できません。

ただし、この基本的な例では:

bool boolVal = /*anything boolean*/;

if (boolVal)
{
  // Do A
}
else if (!boolVal)
{
  // Do B
}
else
{
  // Do C
}

ここで、コンパイラはelseブロックがデッドコードであることを計算できます。そのため、コンパイラーは、デッドコードを理解するのに十分なデータがある場合にのみデッドコードについて警告することができ、指定されたブロックがデッドコードであるかどうかを把握するためにそのデータを適用する方法を知っている必要があります。

編集

コンパイル時にデータが利用できない場合があります。

// File a.cpp
bool boolMethod();

bool boolVal = boolMethod();

if (boolVal)
{
  // Do A
}
else
{
  // Do B
}

//............
// File b.cpp
bool boolMethod()
{
    return true;
}

a.cppのコンパイル中、コンパイラはboolMethod常にが返されることを認識できませんtrue


1
コンパイラが知らないことは厳密に真実ですが、リンカが知ることができるかどうかも尋ねることは質問の精神にあると思います。
Casey Kuball、2015年

1
@Darthfett リンカーの責任ではありません。リンカーは、コンパイルされたコードの内容を分析しません。リンカ(一般的に言えば)はメソッドとグローバルデータをリンクするだけで、内容は関係ありません。ただし、一部のコンパイラには、ソースファイル(ICCなど)を連結してから最適化を実行するオプションがあります。そのような場合、EDITのケースはカバーされますが、このオプションは、特にプロジェクトが大きい場合、コンパイル時間に影響します。
Alex Lop。

この答えは私に誤解を招くようです。あなたは、すべての情報が利用できるわけではないので不可能である2つの例を挙げていますが、情報がそこにあるとしても不可能ではないと言ってはいけませんか?
Anton

@AntonGolovIt osは常に真ではありません。情報が存在する多くの場合、コンパイラーはデッドコードを検出して最適化できます。
Alex Lop。

@abforceはコードのブロックです。それは他の何かだったかもしれません。:)
Alex Lop。

4

コンパイラーは常にいくつかのコンテキスト情報を欠いています。たとえば、double値が2を超えることは決してないことを知っているかもしれません。これは数学関数の機能であるため、ライブラリから使用します。コンパイラーはライブラリー内のコードさえも認識せず、すべての数学関数のすべての機能を知ることはできず、それらを実装するための奇妙で複雑な方法をすべて検出することはできません。


4

コンパイラは必ずしもプログラム全体を見るとは限りません。直接呼び出されない私のプログラムの関数にコールバックする共有ライブラリを呼び出すプログラムがあるかもしれません。

そのため、コンパイル時にライブラリに対して無効になっている関数は、そのライブラリが実行時に変更された場合に有効になる可能性があります。


3

コンパイラーがすべてのデッドコードを正確に排除できる場合、それはインタープリターと呼ばれます。

この単純なシナリオを考えてみましょう:

if (my_func()) {
  am_i_dead();
}

my_func() 任意のコードを含めることができ、コンパイラがtrueまたはfalseを返すかどうかを判断するには、コードを実行するか、コードの実行と機能的に同等の処理を行う必要があります。

コンパイラーの考え方は、コードの部分的な分析のみを実行することであり、これにより、別の実行環境のジョブを簡素化します。完全な分析を実行すると、それはもはやコンパイラではありません。


あなたは関数としてコンパイラを考慮すればc()c(source)=compiled code、およびなど実行している環境r()r(compiled code)=program outputそしてあなたがの値を計算する必要がある任意のソースコードの出力を決定するために、r(c(source code))。計算で任意の入力のc()値の知識がr(c())必要な場合、個別のr()andの必要はありません。そのようなc()関数i()から関数を導出できます。c()i(source)=program output


2

他の人たちは停止問題などについてコメントしています。これらは一般的に機能の一部に適用されます。ただし、タイプ全体(クラスなど)が使用されているかどうかを知るのは困難/不可能である場合があります。

.NET / Java / JavaScriptおよびその他のランタイム駆動環境では、リフレクションを介してロードされる型を停止するものは何もありません。これは、依存性注入フレームワークで人気があり、逆シリアル化または動的モジュールの読み込みに直面して考えるのはさらに困難です。

コンパイラーは、そのようなタイプがロードされるかどうかを知ることができません。それらの名前、実行時に外部構成ファイルから取得される可能性があります。

未使用のコードのサブグラフを安全に削除しようとするツールの一般的な用語であるツリーシェーキングを探してみてください。


私はJavaとJavaScriptについては知りませんが、.NETには実際にはそのようなDI検出用のリシャーププラグインがあります(エージェントモルダーと呼ばれます)。もちろん、構成ファイルを検出することはできませんが、コード内のconfitを検出することはできます(これはより一般的です)。
2015年

2

機能を取る

void DoSomeAction(int actnumber) 
{
    switch(actnumber) 
    {
        case 1: Action1(); break;
        case 2: Action2(); break;
        case 3: Action3(); break;
    }
}

あなたはそれが決して呼ばれactnumberないことを証明することができます...?2Action2()


7
関数の呼び出し元を分析できれば、可能です。
2015年

2
@ablighしかし、コンパイラは通常、すべての呼び出しコードを分析できません。とにかく、たとえそれができたとしても、完全な分析には、考えられるすべての制御フローのシミュレーションが必要になるだけかもしれません。これは、リソースと時間が必要なため、ほとんどの場合不可能です。したがって、理論的にAction2()呼び出されない」という証明が存在する場合でも、実際にその主張を証明することは不可能です。コンパイラで完全に解決することはできません。違いは、「Xが存在する」と「Xを10進数で書くことができる」のようなものです。一部のXについては、前者は真実ですが、後者は決して起こりません。
CiaPan 2015年

これは悪い答えです。他の回答は、かどうかを知ることが不可能であることを証明していactnumber==2ます。この答えは、複雑さを述べることさえせずに難しいと主張するだけです。
MSalters 2015年

1

停止の問題について私は同意しません。実際に到達することは決してないだろうが、私はそのようなコードを死んだとは呼びません。

代わりに、検討してみましょう:

for (int N = 3;;N++)
  for (int A = 2; A < int.MaxValue; A++)
    for (int B = 2; B < int.MaxValue; B++)
    {
      int Square = Math.Pow(A, N) + Math.Pow(B, N);
      float Test = Math.Sqrt(Square);
      if (Test == Math.Trunc(Test))
        FermatWasWrong();
    }

private void FermatWasWrong()
{
  Press.Announce("Fermat was wrong!");
  Nobel.Claim();
}

(タイプとオーバーフローエラーは無視してください)デッドコード?


2
Fermatの最後の定理は1994年に証明されました。したがって、メソッドを正しく実装しても、FermatWasWrongは実行されません。floatの精度の限界に達する可能性があるため、実装でFermatWasWrongが実行されると思います。
Taemyr

@テミールああ!このプログラムはフェルマーの最後の定理を正しくテストしていません。テストの対象となる反例は、N = 3、A = 65536、B = 65536(Test = 0になります)
user253751

@immibisはい、フロートの精度が問題になる前にintがオーバーフローすることを逃しました。
Taemyr、

@immibis投稿の下部に注意してください:タイプとオーバーフローのエラーは無視してください。私は、未解決の問題だと思ったものを決定の基礎として採用していました。コードが完全ではないことはわかっています。とにかくブルートフォースで強制できない問題です。
Loren Pechtel、2015年

-1

この例を見てください:

public boolean isEven(int i){

    if(i % 2 == 0)
        return true;
    if(i % 2 == 1)
        return false;
    return false;
}

コンパイラーは、intが偶数または奇数にしかなり得ないことを知ることができません。したがって、コンパイラーはコードのセマンティクスを理解できなければなりません。これはどのように実装する必要がありますか?コンパイラーは、最低のリターンが実行されないことを保証できません。したがって、コンパイラはデッドコードを検出できません。


1
うーん、本当ですか?これをC#+ ReSharperで書くと、いくつかのヒントが得られます。彼らをフォローすると、最終的にコードが得られますreturn i%2==0;
Thomas Weller

10
あなたの例は納得できないほど単純です。具体的なケースi % 2 == 0i % 2 != 0偶数の整数の値を法(簡単に行うことはまだある)定数についての推論を必要としない、それが唯一の共通部分式除去と一般原則(でも標準化、)が必要if (cond) foo; if (!cond) bar;に簡素化することができますif (cond) foo; else bar;。もちろん、「セマンティクスを理解する」ことは非常に難しい問題ですが、この投稿は、そのことを示しておらず、デッドコードの検出にこの難しい問題を解決する必要があることも示していません。

5
あなたの例では、最適化コンパイラが共通の部分式を見つけ、i % 2それを一時変数に引き出します。次に、2つのifステートメントが相互に排他的でとして記述できることを認識し、if(a==0)...else...可能なすべての実行パスが最初の2つのreturnステートメントを通過するため、3番目のreturnステートメントがデッドコードであることを認識します。(優れた最適化コンパイラーはさらに積極的です。GCCは私のテストコードをビット操作のペアに変換しました)。
マーク

1
この例は私に適しています。これは、コンパイラが実際の状況について知らない場合の例です。同じことがについても言えif (availableMemory()<0) then {dead code}ます。
リトルサンティ

1
@LittleSanti:実際、GCCはあなたが書いたものすべてがデッドコードであることを検出します!それだけではありません{dead code}。GCCは、不可避の符号付き整数オーバーフローがあることを証明することでこれを発見します。したがって、実行グラフのそのアーク上のすべてのコードはデッドコードです。GCCは、そのアークにつながる条件付きブランチを削除することもできます。
MSalters 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.