私がCまたはJavaで使用してきたコンパイラには、デッドコード防止(行が実行されない場合の警告)があります。私の教授は、この問題はコンパイラでは完全に解決できないと言っています。なぜだろうと思っていました。これは理論に基づいたクラスであるため、コンパイラの実際のコーディングにはあまり慣れていません。しかし、私は彼らが何をチェックするのか(可能な入力文字列対許容可能な入力など)、そしてなぜそれが不十分なのかと思っていました。
デッドコードの問題は Halting problem に関連しています。
Alan Turingは、プログラムに与えられる一般的なアルゴリズムを記述し、そのプログラムがすべての入力に対して停止するかどうかを決定することは不可能であることを証明しました。特定の種類のプログラム用にこのようなアルゴリズムを作成できる場合がありますが、すべてのプログラム用ではありません。
これはデッドコードとどのように関係していますか?
停止の問題は、デッドコードを見つける問題に対するreducibleです。つまり、anyプログラムでデッドコードを検出できるアルゴリズムを見つけた場合、そのアルゴリズムを使用してanyプログラムは停止します。それは不可能であることが証明されているため、デッドコードのアルゴリズムを書くことも不可能です。
デッドコードのアルゴリズムを停止問題のアルゴリズムにどのように転送しますか?
シンプル:停止を確認するプログラムの最後にコード行を追加します。デッドコード検出器がこの行がデッドであることを検出した場合、プログラムが停止しないことがわかります。そうでない場合は、プログラムが停止していることがわかります(最後の行に移動してから、追加したコード行に移動します)。
コンパイラは通常、コンパイル時に無効であることが証明できるものをチェックします。たとえば、コンパイル時にfalseと判断できる条件に依存するブロック。または、return
の後の任意のステートメント(同じスコープ内)。
これらは特定のケースであるため、それらのアルゴリズムを記述することができます。より複雑な場合(条件が構文的に矛盾しているかどうかをチェックし、常にfalseを返すかどうかをチェックするアルゴリズムなど)のアルゴリズムを作成することもできますが、それでもすべての可能なケースをカバーするわけではありません。
さて、停止問題の決定不能性の古典的な証明を取り、停止検出器をデッドコード検出器に変更しましょう!
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!");
は実行されないため、このプログラムは実際にdoesデッドコードがあり、検出器が間違っていました。
しかし、true
を返す場合、System.Console.WriteLn("Dead code!");
行が実行されます。プログラムにはコードがもうないため、デッドコードはまったくないため、検出器は間違っていました。
そのため、「デッドコードが存在する」または「デッドコードが存在しない」のみを返すデッドコード検出器は、間違った答えを返すことがあります。
停止の問題があまりにもあいまいな場合は、このように考えてください。
すべての正の整数nに当てはまると信じられているが、すべてのn。良い例は Goldbachの予想 です。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回の反復のオーダーを取ります。上記のプログラムは、最適化せずに何かを出力するのではなく、宇宙の熱死まで実行される(または実行される可能性がある)ので、実際、これは素晴らしい最適化になります。
C++またはJavaにEval
型関数があるかどうかはわかりませんが、多くの言語ではメソッドを呼び出すことができます名前で。次の(想定される)VBAの例を考えてみましょう。
Dim methodName As String
If foo Then
methodName = "Bar"
Else
methodName = "Qux"
End If
Application.Run(methodName)
呼び出されるメソッドの名前は、実行時まで知ることができません。したがって、定義により、コンパイラは特定のメソッドが呼び出されないことを絶対的に確実に知ることができません。
実際、名前でメソッドを呼び出す例を考えると、分岐ロジックは必要ありません。単に言って
Application.Run("Bar")
コンパイラーが判断できる範囲を超えています。コードがコンパイルされると、コンパイラは、特定の文字列値がそのメソッドに渡されることを認識します。実行時までそのメソッドが存在するかどうかを確認しません。メソッドが他の通常のメソッドを介して他の場所で呼び出されない場合、無効なメソッドを見つけようとすると、偽陽性が返される可能性があります。同じ問題は、リフレクションを介してコードを呼び出すことができるすべての言語に存在します。
無条件のデッドコードは、高度なコンパイラによって検出および削除できます。
ただし、条件付きデッドコードもあります。これは、コンパイル時には認識できないコードであり、実行時にのみ検出できます。たとえば、ソフトウェアは、ユーザーの好みに応じて特定の機能を含めたり除外したりするように構成でき、特定のシナリオではコードの特定のセクションが死んでいるように見えます。それは本当のデッドコードではありません。
テストを実行したり、依存関係を解決したり、条件付きデッドコードを削除したり、実行時に効率的な有用なコードを再結合したりできる特定のツールがあります。これは、動的なデッドコード除去と呼ばれます。しかし、ご覧のとおり、コンパイラの範囲を超えています。
簡単な例:
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
を返すことを知ることができません。
コンパイラは常にいくつかのコンテキスト情報を欠いています。例えば。 double値が2を超えないことはご存知かもしれませんが、これは数学関数の機能であるため、ライブラリから使用します。コンパイラーはライブラリー内のコードを見ることさえできず、すべての数学関数のすべての機能を知ることはできず、それらを実装するためのすべての奇妙で複雑な方法を検出することはできません。
コンパイラは必ずしもプログラム全体を見るとは限りません。共有ライブラリを呼び出すプログラムを作成できます。共有ライブラリは、直接呼び出されないプログラム内の関数にコールバックします。
そのため、実行時にライブラリが変更された場合、コンパイル対象のライブラリに関して無効になっている関数が有効になる可能性があります。
コンパイラがすべてのデッドコードを正確に排除できる場合、インタープリターと呼ばれます。
次の簡単なシナリオを検討してください。
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()
とc()
は不要です。関数i()
from c()
from i(source)=program output
。
機能を取る
void DoSomeAction(int actnumber)
{
switch(actnumber)
{
case 1: Action1(); break;
case 2: Action2(); break;
case 3: Action3(); break;
}
}
actnumber
が2
にならないことを証明して、Action2()
が呼び出されないようにすることはできますか?
他の人は、停止問題などについてコメントしています。これらは通常、機能の一部に適用されます。ただし、型全体(クラス/など)が使用されているかどうかを判断するのは難しい/不可能な場合があります。
.NET/Java/JavaScriptおよびその他のランタイム駆動型環境では、リフレクションを介してロードされるタイプを停止するものは何もありません。これは、依存性注入フレームワークで人気があり、逆シリアル化または動的モジュールのロードに直面して推論するのがさらに困難です。
コンパイラは、そのような型がロードされるかどうかを知ることができません。それらの名前couldは実行時の外部設定ファイルに由来します。
tree shakeを検索したい場合があります。これは、コードの未使用のサブグラフを安全に削除しようとするツールの一般的な用語です。
停止する問題については同意しません。実際には到達することはありませんが、私はそのようなコードを死んだとは呼ばないでしょう。
代わりに、次のことを考慮してください。
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();
}
(タイプとオーバーフローエラーを無視します)デッドコード?