Les compilateurs que j'utilisais en C ou Java ont une prévention de code mort (avertissement lorsqu'une ligne ne sera jamais exécutée). Mon professeur dit cependant que ce problème ne peut jamais être entièrement résolu par les compilateurs. Je me demandais pourquoi. Je ne suis pas trop familier avec le codage réel des compilateurs car il s'agit d'une classe basée sur la théorie. Mais je me demandais ce qu'ils vérifient (comme les chaînes d'entrée possibles par rapport aux entrées acceptables, etc.), et pourquoi cela est insuffisant.
compiler-theory
Apprenant
la source
la source
if (isPrime(1234234234332232323423)){callSomething();}
ce code appellera-t-il ou non quelque chose? Il existe de nombreux autres exemples, où décider si une fonction est appelée est de loin plus cher que de simplement l'inclure dans le programme.public static void main(String[] args) {int counterexample = findCollatzConjectureCounterexample(); System.out.println(counterexample);}
<- est le code mort d'appel println? Même les humains ne peuvent pas résoudre celui-là!Réponses:
Le problème de code mort est lié au problème d'arrêt .
Alan Turing a prouvé qu'il est impossible d'écrire un algorithme général auquel sera attribué un programme et de pouvoir 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 Halting est réductible au problème de la recherche de code mort. Autrement dit, si vous trouvez un algorithme qui peut détecter le code mort dans n'importe quel programme, vous pouvez utiliser cet algorithme pour tester si un programme va s'arrêter. Comme cela s'est avéré impossible, il s'ensuit que l'écriture d'un algorithme pour le code mort est également impossible.
Comment transférez-vous un algorithme pour le code mort dans un algorithme pour le problème d'arrêt?
Simple: vous ajoutez une ligne de code après la fin du programme dont vous souhaitez vérifier l'arrêt. Si votre détecteur de code mort détecte que cette ligne est morte, alors 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 (arrive à la dernière ligne, puis à votre ligne de code ajoutée).
Les compilateurs vérifient généralement les éléments qui peuvent être prouvés au moment de la compilation comme morts. Par exemple, des blocs qui dépendent de conditions qui peuvent être déterminées comme fausses au moment de la compilation. Ou toute instruction 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 compliqués (comme un algorithme qui vérifie si une condition est syntaxiquement une contradiction et par conséquent retournera toujours faux), mais cela ne couvrirait pas tous les cas possibles.
la source
256^(2^64)
états estO(1)
, donc la détection de code mort peut être effectuée en temps polynomial.Eh 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 de code mort!
Programme C #
S'il
YourVendor.Compiler.HasDeadCode(quine_text)
revientfalse
, alors la ligneSystem.Console.WriteLn("Dead code!");
ne sera jamais exécutée, donc ce programme a en fait un code mort et le détecteur était erroné.Mais s'il revient
true
, alors la ligneSystem.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 s'est trompé.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.
la source
Si le problème d'arrêt est trop obscur, pensez-y de cette façon.
Prenons un problème mathématique qui est censé être vrai pour tous les n entiers positifs , mais qui n'a pas été prouvé pour tous les n . Un bon exemple serait la conjecture de Goldbach, selon laquelle tout entier pair 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):
L'implémentation de
isGoldbachsConjectureTrueFor()
est laissée comme un exercice pour le lecteur mais à cette fin pourrait être 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:
(c'est-à-dire une boucle infinie) ou
car la conjecture de Goldbach doit être vraie ou fausse. Si un compilateur pouvait toujours éliminer le code mort, il y aurait certainement du code mort à éliminer ici dans les deux cas. Cependant, ce faisant, votre compilateur devra au moins résoudre des problèmes arbitrairement difficiles. Nous pourrions fournir des problèmes prouvablement dur qu'il faudrait résoudre (par exemple les problèmes NP-complets) pour déterminer quel bit de code à éliminer. Par exemple, si nous prenons ce programme:
nous savons que le programme imprimera soit "Valeur SHA trouvée" soit "Valeur SHA non trouvée" (points bonus si vous pouvez me dire laquelle est vraie). Cependant, pour qu'un compilateur puisse raisonnablement optimiser cela prendrait de l'ordre de 2 ^ 2048 itérations. Ce serait en fait une excellente optimisation car je prédis que le programme ci-dessus fonctionnerait (ou pourrait) fonctionner jusqu'à la mort thermique de l'univers plutôt que d'imprimer quoi que ce soit sans optimisation.
la source
sha256
retourner un tableau d'octets et les tableaux d'octets ne sont pas comparables aux chaînes de votre langue.Implementation of isGoldbachsConjectureTrueFor() is left as an exercise for the reader
Cela m'a fait rire.Je ne sais pas si C ++ ou Java ont une
Eval
fonction de type, mais de nombreux langages vous permettent d'appeler des méthodes par leur nom . Considérez l'exemple VBA (artificiel) suivant.Le nom de la méthode à appeler est impossible à connaître jusqu'à 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 de l'appel d'une méthode par son nom, la logique de branchement n'est même pas nécessaire. Dire simplement
C'est plus que ce que le compilateur peut déterminer. Lorsque le code est compilé, tout ce que le compilateur sait, c'est qu'une certaine valeur de chaîne est transmise à cette méthode. Il ne vérifie pas si cette méthode existe jusqu'à l'exécution. Si la méthode n'est pas appelée ailleurs, via des 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 via la réflexion.
la source
Le code mort inconditionnel peut être détecté et supprimé par des compilateurs avancés.
Mais il existe également un code mort conditionnel. Il s'agit d'un code qui 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, ce qui rend 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 qui peuvent effectuer des tests, résoudre les dépendances, supprimer le code mort conditionnel et recombiner le code utile lors de l'exécution pour plus d'efficacité. C'est ce qu'on appelle l'élimination dynamique du code mort. Mais comme vous pouvez le voir, cela dépasse le cadre des compilateurs.
la source
Un exemple simple:
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
else
bloc ne sera jamais exécuté.Cependant, dans cet exemple de base:
Ici, le compilateur peut calculer si le
else
bloc 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 et il doit également savoir comment appliquer ces données afin de déterminer si le bloc donné est un code mort.ÉDITER
Parfois, les données ne sont tout simplement pas disponibles au moment de la compilation:
Lors de la compilation de a.cpp, le compilateur ne peut pas savoir que cela
boolMethod
revient toujourstrue
.la source
Le compilateur manquera toujours d'informations sur le 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 il 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.
la source
Le compilateur ne voit pas nécessairement tout le programme. Je pourrais avoir un programme qui appelle une bibliothèque partagée, 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 avec laquelle elle est compilée pourrait devenir active si cette bibliothèque était modifiée au moment de l'exécution.
la source
Si un compilateur pouvait éliminer avec précision tout le code mort, il serait appelé interprète .
Considérez ce scénario simple:
my_func()
peut contenir du code arbitraire et pour que le compilateur détermine s'il renvoie vrai ou faux, il devra soit exécuter le code, soit faire quelque chose qui est fonctionnellement équivalent à l'exécution du code.L'idée d'un compilateur est qu'il n'effectue qu'une analyse partielle du code, simplifiant ainsi le travail d'un environnement d'exécution distinct. 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 d'exécution commer()
, oùr(compiled code)=program output
, alors pour déterminer la sortie de tout code source, vous devez calculer la valeur der(c(source code))
. Si calculc()
nécessite la connaissance de la valeurr(c())
pour toute entrée, il n'y a pas besoin d'un séparér()
etc()
vous pouvez simplement tirer une fonctioni()
dec()
telle sorte quei(source)=program output
.la source
D'autres ont commenté le problème de l'arrêt, etc. Celles-ci s'appliquent généralement à des portions de fonctions. Cependant, il peut être difficile / impossible de savoir si même un type entier (classe / etc) est utilisé ou non.
Dans .NET / Java / JavaScript et d'autres environnements pilotés par le runtime, il n'y a rien qui empêche le chargement des types via la réflexion. Ceci est populaire avec les frameworks d'injection de dépendance, et est encore plus difficile à 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 provenir de fichiers de configuration externes lors de l'exécution.
Vous voudrez peut-être chercher autour de l' arbre qui est un terme courant pour les outils qui tentent de supprimer en toute sécurité les sous-graphiques de code inutilisés.
la source
Prenez une fonction
Pouvez-vous prouver que
actnumber
cela ne sera jamais2
ainsi que ceAction2()
n'est jamais appelé ...?la source
Action2()
ne sera jamais appelé», il est impossible de prouver la revendication dans la pratique - ne peut pas être entièrement résolu par un compilateur . La différence est comme «il existe un nombre X» vs «nous pouvons écrire le nombre X en décimal». Pour certains X, ce dernier ne se produira jamais bien que le premier soit vrai.actnumber==2
. Cette réponse prétend simplement que c'est difficile sans même mentionner une complexité.Je ne suis pas d'accord sur le problème de l'arrêt. Je n'appellerais pas un tel code mort même si en réalité il ne sera jamais atteint.
Considérons plutôt:
(Ignorer les erreurs de type et de débordement) Code mort?
la source
Regardez cet exemple:
Le compilateur ne peut pas savoir qu'un int ne peut être que pair ou impair. Par conséquent, le compilateur doit être capable de comprendre la sémantique de votre code. Comment cela devrait-il être mis en œuvre? Le compilateur ne peut pas garantir que le retour le plus bas ne sera jamais exécuté. Par conséquent, le compilateur ne peut pas détecter le code mort.
la source
return i%2==0;
.i % 2 == 0
eti % 2 != 0
ne nécessite même pas de raisonnement sur la valeur d'un module entier une constante (ce qui est toujours facile à faire), il ne nécessite que l'élimination de la sous-expression commune et le principe général (canonisation, même) quiif (cond) foo; if (!cond) bar;
peut être simplifiéif (cond) foo; else bar;
. Bien sûr, "comprendre la sémantique" est un problème très difficile, mais ce post ne montre pas qu'il l'est, ni ne montre que la résolution de ce problème difficile est nécessaire pour la détection de code mort.i % 2
- expression commune et la retirera dans une variable temporaire. Il reconnaîtra alors que les deuxif
instructions s'excluent mutuellement et peuvent être écrites commeif(a==0)...else...
, puis remarquera que tous les chemins d'exécution possibles passent par les deux premièresreturn
instructions et donc la troisièmereturn
instruction est du code mort. (Un bon compilateur d'optimisation est encore plus agressif: GCC a transformé mon code de test en une paire d'opérations de manipulation de bits).if (availableMemory()<0) then {dead code}
.{dead code}
partie. GCC le découvre en prouvant qu'il y a un débordement d'entier signé inévitable. Tout le code sur cet arc dans le graphe d'exécution est donc du code mort. GCC peut même supprimer la branche conditionnelle qui mène à cet arc.