Les compilateurs que j'ai utilisés en C ou Java ont une prévention contre le code mort (avertissement lorsqu'une ligne ne sera jamais exécutée). Mon professeur dit que ce problème ne peut jamais être entièrement résolu par les compilateurs. Je me demandais pourquoi, je ne connaissais pas trop bien le codage des compilateurs, car il s’agissait d’une classe théorique, mais je me demandais ce qu’ils vérifiaient (comme les chaînes d’entrée possibles par rapport aux entrées acceptables, etc.), et pourquoi c'est insuffisant.
Le problème de code mort est lié au problème d'arrêt .
Alan Turing a prouvé qu'il était impossible d'écrire un algorithme général auquel un programme serait attribué et capable de décider si ce programme s'arrête pour toutes les entrées. Vous pourrez peut-être écrire un tel algorithme pour des types de programmes spécifiques, mais pas pour tous les programmes.
Quel est le lien avec le code mort?
Le problème de Halting est réductible au problème de la recherche de code mort. Autrement dit, si vous trouvez un algorithme capable de détecter du code mort dans n’importe quel programme , vous pouvez utiliser cet algorithme pour vérifier si tout programme sera interrompu. Comme il a été prouvé que cela était impossible, il s'ensuit que l'écriture d'un algorithme pour code mort est également impossible.
Comment transférer un algorithme de code mort en un algorithme pour le problème Halting?
Simple: vous ajoutez une ligne de code à la fin du programme que vous souhaitez vérifier. Si votre détecteur de code mort détecte que cette ligne est morte, vous savez que le programme ne s’arrête pas. Si ce n'est pas le cas, vous savez que votre programme s'arrête (passe à la dernière ligne, puis à votre ligne de code ajoutée).
Les compilateurs recherchent généralement des choses qui peuvent être prouvées mortes au moment de la compilation. Par exemple, les blocs qui dépendent de conditions pouvant être définies comme étant fausses au moment de la compilation. Ou toute autre déclaration après un return
(dans la même portée).
Ce sont des cas spécifiques, et il est donc possible d'écrire un algorithme pour eux. Il peut être possible d'écrire des algorithmes pour des cas plus complexes (comme un algorithme qui vérifie si une condition est syntaxiquement une contradiction et retournera donc toujours faux), mais cela ne couvrirait pas tous les cas possibles.
Bien, prenons la preuve classique de l’indécidabilité du problème d’arrêt et changeons le détecteur d’arrêt en détecteur à code mort!
programme 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!");
}
}
}
Si YourVendor.Compiler.HasDeadCode(quine_text)
retourne false
, alors la ligne System.Console.WriteLn("Dead code!");
ne sera jamais exécutée, donc ce programme fonctionne réellement ont un code mort, et le détecteur était faux.
Mais s'il retourne true
, alors la ligne System.Console.WriteLn("Dead code!");
sera exécutée, et comme il n'y a plus de code dans le programme, il n'y a plus de code mort, donc encore une fois, le détecteur était faux.
Donc, voilà, un détecteur de code mort qui ne renvoie que "Il y a du code mort" ou "Il n'y a pas de code mort" doit parfois donner de mauvaises réponses.
Si le problème d’arrêt est trop obscur, réfléchissez de cette façon.
Prenons un problème mathématique que l’on pense être vrai pour tous les entiers positifs n , mais qui n’a pas été prouvé vrai pour tous n . Un bon exemple serait conjecture de Goldbach , tout nombre entier positif supérieur à deux peut être représenté par la somme de deux nombres premiers. Ensuite (avec une bibliothèque bigint appropriée), exécutez ce programme (le pseudocode suit):
for (BigInt n = 4; ; n+=2) {
if (!isGoldbachsConjectureTrueFor(n)) {
print("Conjecture is false for at least one value of n\n");
exit(0);
}
}
L'implémentation de isGoldbachsConjectureTrueFor()
est laissée comme un exercice pour le lecteur mais à cette fin, une simple itération sur tous les nombres premiers inférieurs à n
Maintenant, logiquement, ce qui précède doit être soit l'équivalent de:
for (; ;) {
}
(c'est-à-dire une boucle infinie) ou
print("Conjecture is false for at least one value of n\n");
comme la conjecture de Goldbach doit être soit vrai ou pas vrai. Si un compilateur pouvait toujours éliminer le code mort, il y aurait certainement un code mort à éliminer ici dans les deux cas. Cependant, votre compilateur aurait au moins besoin de résoudre des problèmes difficiles. Nous pourrions fournir des problèmes de manière prouvable difficiles à résoudre (par exemple, des problèmes NP-complets) pour déterminer le bit de code à éliminer. Par exemple si nous prenons ce programme:
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");
nous savons que le programme affichera soit "Trouvé SHA valeur") ou "Introuvable SHA valeur" (points bonus si vous pouvez me dire lequel est Cependant, pour qu’un compilateur puisse raisonnablement optimiser cela prendrait de l’ordre de 2 ^ 2048 it. Ce serait en fait une grande optimisation, car je prédis que le programme ci-dessus serait (ou pourrait) être exécuté jusqu’à la mort de la chaleur. de l'univers plutôt que d'imprimer quoi que ce soit sans optimisation.
Je ne sais pas si C++ ou Java a une fonction type Eval
, mais de nombreux langages vous permettent d'appeler des méthodes par nom. Considérez le exemple suivant (artificiel) VBA.
Dim methodName As String
If foo Then
methodName = "Bar"
Else
methodName = "Qux"
End If
Application.Run(methodName)
Le nom de la méthode à appeler est impossible à connaître avant l'exécution. Par conséquent, par définition, le compilateur ne peut pas savoir avec une certitude absolue qu’une méthode particulière n’est jamais appelée.
En fait, étant donné l'exemple d'appeler une méthode par son nom, la logique de branchement n'est même pas nécessaire. Dire simplement
Application.Run("Bar")
Est plus que le compilateur peut déterminer. Lorsque le code est compilé, tout ce que le compilateur sait est qu’une certaine valeur de chaîne est transmise à cette méthode. Il ne vérifie pas si cette méthode existe jusqu'au moment de l'exécution. Si la méthode n'est pas appelée ailleurs, par le biais de méthodes plus normales, une tentative de recherche de méthodes mortes peut renvoyer des faux positifs. Le même problème existe dans n’importe quel langage qui permet d’appeler du code par réflexion.
Le code mort inconditionnel peut être détecté et supprimé par des compilateurs avancés.
Mais il existe également un code mort conditionnel. Ce code ne peut pas être connu au moment de la compilation et ne peut être détecté que pendant l'exécution. Par exemple, un logiciel peut être configurable pour inclure ou exclure certaines fonctionnalités en fonction des préférences de l'utilisateur, rendant certaines sections de code apparemment mortes dans des scénarios particuliers. Ce n'est pas un vrai code mort.
Il existe des outils spécifiques permettant de tester, de résoudre les dépendances, de supprimer le code mort conditionnel et de recombiner le code utile au moment de l'exécution pour améliorer l'efficacité. Cela s'appelle l'élimination dynamique du code mort. Mais comme vous pouvez le constater, cela dépasse la portée des compilateurs.
Un exemple simple:
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;
}
Supposons maintenant que le port 0x100 est conçu pour renvoyer uniquement 0 ou 1. Dans ce cas, le compilateur ne peut pas comprendre que le bloc else
ne sera jamais exécuté.
Cependant, dans cet exemple de base:
bool boolVal = /*anything boolean*/;
if (boolVal)
{
// Do A
}
else if (!boolVal)
{
// Do B
}
else
{
// Do C
}
Ici, le compilateur peut calculer que le bloc else
est un code mort. Ainsi, le compilateur peut avertir du code mort uniquement s’il dispose de suffisamment de données pour comprendre le code mort. Il doit également savoir comment appliquer ces données afin de déterminer si le bloc en question est un code mort.
EDIT
Parfois, les données ne sont tout simplement pas disponibles au moment de la compilation:
// File a.cpp
bool boolMethod();
bool boolVal = boolMethod();
if (boolVal)
{
// Do A
}
else
{
// Do B
}
//............
// File b.cpp
bool boolMethod()
{
return true;
}
Lors de la compilation de a.cpp, le compilateur ne peut pas savoir que boolMethod
renvoie toujours true
.
Le compilateur manquera toujours de certaines informations de contexte. Par exemple. vous savez peut-être qu’une valeur double n’excède jamais 2, car c’est une caractéristique de la fonction mathématique que vous utilisez à partir d’une bibliothèque. Le compilateur ne voit même pas le code dans la bibliothèque et ne peut jamais connaître toutes les fonctionnalités de toutes les fonctions mathématiques et détecter toutes les manières compliquées et compliquées de les implémenter.
Le compilateur ne voit pas nécessairement tout le programme. Je pourrais avoir un programme qui appelle une bibliothèque partagée, ce qui rappelle une fonction de mon programme qui n'est pas appelée directement.
Ainsi, une fonction qui est morte par rapport à la bibliothèque pour laquelle elle est compilée pourrait devenir active si cette bibliothèque était changée à l'exécution.
Si un compilateur pouvait éliminer tout le code mort avec précision, il serait appelé un interprète.
Considérez ce scénario simple:
if (my_func()) {
am_i_dead();
}
my_func()
peut contenir du code arbitraire. Pour que le compilateur détermine s'il renvoie true ou false, il doit exécuter le code ou exécuter une opération qui équivaut fonctionnellement à son exécution.
L'idée d'un compilateur est qu'il n'effectue qu'une analyse partielle du code, simplifiant ainsi le travail d'un environnement en cours d'exécution séparé. Si vous effectuez une analyse complète, ce n'est plus un compilateur.
Si vous considérez le compilateur comme une fonction c()
, où c(source)=compiled code
, et l'environnement en cours d'exécution comme r()
, où r(compiled code)=program output
, déterminez ensuite la sortie pour tout code source que vous devez calculer la valeur de r(c(source code))
. Si le calcul de c()
nécessite la connaissance de la valeur de r(c())
pour toute entrée, il n'est pas nécessaire de disposer d'une r()
et d'une c()
distinctes: vous pouvez simplement dériver une fonction i()
de c()
telle que i(source)=program output
.
Prendre une fonction
void DoSomeAction(int actnumber)
{
switch(actnumber)
{
case 1: Action1(); break;
case 2: Action2(); break;
case 3: Action3(); break;
}
}
Pouvez-vous prouver que actnumber
ne sera jamais 2
De sorte que Action2()
ne soit jamais appelé ...?
D'autres ont commenté le problème persistant, etc. Celles-ci s'appliquent généralement à des portions de fonctions. Cependant, il peut être difficile/impossible de savoir si un type même (classe/etc) est utilisé ou non.
Dans .NET/Java/JavaScript et d'autres environnements d'exécution, rien n'empêche le chargement de types par réflexion. Ceci est populaire avec les infrastructures d'injection de dépendance, et il est encore plus difficile de raisonner face à la désérialisation ou au chargement de module dynamique.
Le compilateur ne peut pas savoir si de tels types seraient chargés. Leurs noms pourraient proviennent de fichiers de configuration externes au moment de l'exécution.
Vous aimerez peut-être chercher secouement de l’arbre, terme communément utilisé pour désigner les outils qui tentent de supprimer en toute sécurité les sous-graphiques inutilisés du code.
Je ne suis pas d’accord sur le problème d’arrêt. Je n'appellerais pas un tel code mort même si, en réalité, il ne sera jamais atteint.
Au lieu de cela, considérons:
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();
}
(Ignorez les erreurs de type et de débordement) Code mort?