Ich plane einen Winterkurs zu verschiedenen Themen, von denen eines Compiler sein wird. Nun, ich bin auf dieses Problem gestoßen, als ich über Aufträge nachgedacht habe, die während des gesamten Quartals zu vergeben waren, aber es hat mich ratlos gemacht, sodass ich es stattdessen als Beispiel verwenden könnte.
public class DeadCode {
public static void main(String[] args) {
return;
System.out.println("This line won't print.");
}
}
Im obigen Programm ist es offensichtlich, dass die print-Anweisung wegen der nie ausgeführt wird return
. Compiler geben manchmal Warnungen oder Fehler über toten Code aus. Beispielsweise wird der obige Code in Java nicht kompiliert. Der Javac-Compiler erkennt jedoch nicht alle Instanzen von totem Code in jedem Programm. Wie würde ich beweisen, dass kein Compiler dies kann?
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
Antworten:
Es kommt alles von der Unentscheidbarkeit des Halteproblems. Angenommen, wir haben eine "perfekte" Dead-Code-Funktion, eine Turing-Maschine M und eine Eingabezeichenfolge x und eine Prozedur, die ungefähr so aussieht:
Wenn M für immer läuft, löschen wir die print-Anweisung, da wir sie nie erreichen werden. Wenn M nicht für immer läuft, müssen wir die print-Anweisung beibehalten. Wenn wir also einen Dead-Code-Entferner haben, können wir damit auch das Halting-Problem lösen, sodass wir wissen, dass es keinen solchen Dead-Code-Entferner geben kann.
Wir umgehen dies durch "konservative Annäherung". In meinem obigen Turing Machine-Beispiel können wir also davon ausgehen, dass M on x möglicherweise beendet wird. Gehen Sie also auf Nummer sicher und entfernen Sie die print-Anweisung nicht. In Ihrem Beispiel wissen wir, dass unabhängig davon, welche Funktionen angehalten werden oder nicht, diese print-Anweisung auf keinen Fall erreicht werden kann.
In der Regel wird dazu ein "Kontrollflussdiagramm" erstellt. Wir machen vereinfachende Annahmen, wie "das Ende einer while-Schleife ist mit dem Anfang und der Anweisung danach verbunden", auch wenn sie für immer oder nur einmal ausgeführt wird und nicht beide besucht. In ähnlicher Weise nehmen wir an, dass eine if-Anweisung alle ihre Zweige erreichen kann, auch wenn in Wirklichkeit einige davon niemals verwendet werden. Diese Art von Vereinfachungen ermöglicht es uns, "offensichtlich toten Code" wie in dem von Ihnen angegebenen Beispiel zu entfernen, ohne dass wir uns dafür entscheiden müssen.
Um einige Verwirrungen aus den Kommentaren zu klären:
Wie Raphael in meinem Beispiel sagt, betrachten wir die Turing-Maschine als Eingabe. Die Idee ist, dass wir, wenn wir einen perfekten DCE-Algorithmus hätten, das Codefragment konstruieren könnten, das ich für jede Turing-Maschine gebe , und dass ein DCE das Stopp-Problem lösen würde.
Für das Problem, das njzk2 aufwirft: Sie haben absolut Recht, in diesem Fall können Sie feststellen, dass es keine Möglichkeit gibt, eine Aussage zu treffen, nachdem die Rückgabe erreicht wurde. Dies liegt daran, dass es so einfach ist, dass wir seine Nichterreichbarkeit mit Hilfe von Kontrollflussgraphenbeschränkungen beschreiben können (dh es gibt keine ausgehenden Kanten aus einer return-Anweisung). Es gibt jedoch keinen perfekten Dead Code-Eliminator, der den gesamten nicht verwendeten Code eliminiert.
Für TomášZato: Es ist nicht wirklich ein eingabeabhängiger Beweis. Vielmehr interpretieren Sie es als "Forall". Es funktioniert wie folgt: Nehmen wir an, wir haben einen perfekten DCE-Algorithmus. Wenn Sie mir eine beliebige Turing-Maschine M geben und x eingeben, kann ich meinen DCE-Algorithmus verwenden, um zu bestimmen, ob M anhält, indem ich den obigen Codeausschnitt konstruiere und sehe, ob die print-Anweisung entfernt ist. Diese Technik, einen Parameter willkürlich zu lassen, um eine forall-Anweisung zu beweisen, ist in Mathematik und Logik üblich.
Ich verstehe TomášZato nicht ganz, dass Code endlich ist. Sicher ist der Code endlich, aber ein perfekter DCE-Algorithmus muss für jeden Code gelten, der eine unendliche Menge ist. Während der Code selbst endlich ist, sind die möglichen Mengen von Eingaben ebenso unendlich, wie die mögliche Laufzeit des Codes.
Wenn man bedenkt, dass der letzte Zweig nicht tot ist: Es ist in Bezug auf die "konservative Annäherung", von der ich spreche, sicher, aber es reicht nicht aus, alle Instanzen von totem Code zu erkennen, wenn das OP danach fragt.
Betrachten Sie Code wie folgt:
Natürlich können wir entfernen,
print "goodbye"
ohne das Verhalten des Programms zu ändern. Es ist also toter Code. Wenn es jedoch einen anderen Funktionsaufruf als(true)
in derwhile
Bedingung gibt, wissen wir nicht, ob wir ihn entfernen können oder nicht, was zur Unentscheidbarkeit führt.Beachten Sie, dass ich mir das nicht selbst einfallen lasse. Es ist ein bekanntes Ergebnis in der Compilertheorie. Es wird in The Tiger Book besprochen . (Möglicherweise können Sie in Google-Büchern sehen, worüber sie sprechen .
quelle
Dies ist eine Wendung in Bezug auf die Antwort von jmite, die die mögliche Verwirrung über die Nichtbeendigung umgeht. Ich gebe ein Programm, das sich immer selbst anhält, möglicherweise toten Code hat, aber wir können nicht (immer) algorithmisch entscheiden, ob dies der Fall ist.
Berücksichtigen Sie die folgende Klasse von Eingaben für die Dead-Code-ID:
Da
M
undx
behoben sind,simulateMs
hat Dead Code mitreturn 0
genau dann, wennM
nicht halt aufx
.Dies führt uns sofort zu einer Reduzierung des Halteproblems auf die Überprüfung auf toten Code: Wenn TM als Instanz für das Halteproblem angegeben ist, erstellen Sie das obige Programm mit dem Code - es hat toten Code, wenn nicht von selbst anhält Code.M MM M M
x
Daher ist die Dead-Code-Überprüfung nicht berechenbar.
Für den Fall, dass Sie mit der Reduktion als Proof-Technik in diesem Zusammenhang nicht vertraut sind, empfehle ich unser Referenzmaterial .
quelle
Eine einfache Möglichkeit, diese Art von Eigenschaft zu demonstrieren, ohne sich in Details zu verstricken, ist die Verwendung des folgenden Lemmas:
Lemma: Für jeden Compiler C für eine Turing-complete-Sprache gibt es eine Funktion,
undecidable_but_true()
die keine Argumente akzeptiert und den booleschen Wert true zurückgibt, sodass C nicht vorhersagen kann, obundecidable_but_true()
true oder false zurückgegeben wird.Beachten Sie, dass die Funktion vom Compiler abhängt. Bei einer gegebenen Funktion
undecidable_but_true1()
kann ein Compiler immer mit dem Wissen erweitert werden, ob diese Funktion true oder false zurückgibt. Es gibt jedoch immer eine andere Funktionundecidable_but_true2()
, die nicht behandelt wird.Beweis: Nach dem Satz von Rice ist die Eigenschaft „Diese Funktion gibt wahr zurück“ unentscheidbar. Daher kann ein statischer Analysealgorithmus diese Eigenschaft nicht für alle möglichen Funktionen bestimmen.
Fazit: Bei einem Compiler C enthält das folgende Programm toten Code, der nicht erkannt werden kann:
Ein Hinweis zu Java: Die Java-Sprachanweisung, dass Compiler bestimmte Programme ablehnen, die nicht erreichbaren Code enthalten, und sinnvollerweise, dass dieser Code an allen erreichbaren Punkten bereitgestellt wird (z. B. muss der Kontrollfluss in einer nicht ungültigen Funktion mit einer
return
Anweisung enden ). Die Sprache gibt genau an, wie die Analyse des nicht erreichbaren Codes durchgeführt wird. Wenn dies nicht der Fall wäre, wäre es unmöglich, tragbare Programme zu schreiben. Gegeben ein Programm der FormEs muss angegeben werden, in welchen Fällen auf den nicht erreichbaren Code ein anderer Code folgen muss und in welchen Fällen auf keinen Code. Ein Beispiel für ein Java-Programm, das Code enthält, der nicht erreichbar ist, aber den Java-Compilern nicht auffällt, ist Java 101:
quelle
day_of_week
nicht erreichbar ist.Die Antwort von jmite bezieht sich darauf, ob das Programm jemals eine Berechnung beenden wird - nur weil es unendlich ist, würde ich den Code nicht aufrufen, wenn er tot ist.
Es gibt jedoch einen anderen Ansatz: Ein Problem, für das es eine Antwort gibt, das jedoch nicht bekannt ist:
Diese Routine ohne Zweifel nicht enthält toten Code - die Funktion wird eine Antwort zurück , das einen Weg führt aber nicht die andere. Viel Glück beim Finden! Mein Gedächtnis ist kein theoretischer Computer, der dies innerhalb der Lebensdauer des Universums lösen kann.
Ausführlicher:
Die
Evaluate()
Funktion berechnet, welche Seite eine Schachpartie gewinnt, wenn beide Seiten perfekt spielen (mit maximaler Suchtiefe).Normalerweise schauen die Schachbewerter bei jeder möglichen Bewegung in einer bestimmten Tiefe nach vorn und versuchen dann, das Spielfeld an dieser Stelle zu bewerten (manchmal kann das Erweitern bestimmter Zweige, wenn sie auf halbem Wege durch einen Austausch oder dergleichen schauen, eine sehr verzerrte Wahrnehmung hervorrufen.) Da die tatsächliche maximale Tiefe Ist 17695 Halbzüge die Suche erschöpfend, durchläuft sie jede mögliche Schachpartie. Da alle Spiele zu Ende sind, gibt es kein Problem zu entscheiden, wie gut ein Brett ist (und daher keinen Grund, die Bewertungslogik des Brettes zu überprüfen - sie wird niemals aufgerufen), das Ergebnis ist entweder ein Gewinn, ein Verlust oder ein Verlust Gleichstand. Wenn das Ergebnis ein Unentschieden ist, ist das Spiel fair, wenn das Ergebnis kein Unentschieden ist, ist es ein unfaires Spiel. Um es ein bisschen zu erweitern, bekommen wir:
Beachten Sie auch, dass der Compiler praktisch nicht erkennen kann, dass Chessboard.Score () toter Code ist. Die Kenntnis der Schachregeln ermöglicht es uns Menschen, dies herauszufinden, aber um dies herauszufinden, muss man wissen, dass MakeMove die Stückzahl niemals erhöhen kann und dass Chessboard.Draw () true zurückgibt, wenn die Stückzahl zu lange statisch bleibt .
Beachten Sie, dass die Suchtiefe in Halbzügen und nicht in ganzen Zügen angegeben ist. Dies ist normal für diese Art von KI-Routine, da es sich um eine O (x ^ n) -Routine handelt. Das Hinzufügen einer weiteren Suchschicht hat einen großen Einfluss darauf, wie lange die Ausführung dauert.
quelle
Ich denke, in einem Computerkurs ist der Begriff des toten Codes im Kontext des Verständnisses des Unterschieds zwischen Kompilierungszeit und Laufzeit interessant!
Ein Compiler kann feststellen, wann Code vorhanden ist, der in keinem Kompilierungsszenario jemals verarbeitet werden kann, dies jedoch nicht zur Laufzeit. Eine einfache while-Schleife mit Benutzereingabe für den Loop-Break-Test zeigt dies.
Wenn ein Compiler tatsächlich feststellen könnte, dass der Code zur Laufzeit tot ist (dh Turing vollständig erkennt), dann gibt es ein Argument, dass der Code niemals ausgeführt werden muss, da der Job bereits erledigt ist!
Das Vorhandensein von Code, der die Dead-Code-Prüfungen zur Kompilierungszeit besteht, verdeutlicht die Notwendigkeit einer pragmatischen Überprüfung der Eingaben und der allgemeinen Codierungshygiene (in der realen Welt realer Projekte).
quelle