Los compiladores que he estado usando en C o Java tienen prevención de código muerto (advertencia cuando una línea nunca se ejecutará). Mi profesor dice que este problema nunca puede ser resuelto completamente por los compiladores. Me preguntaba por qué es eso. No estoy muy familiarizado con la codificación real de los compiladores, ya que esta es una clase basada en la teoría. Pero me preguntaba qué verifican (como posibles cadenas de entrada frente a entradas aceptables, etc.) y por qué eso es insuficiente.
compiler-theory
Aprendiz
fuente
fuente
if (isPrime(1234234234332232323423)){callSomething();}
¿Este código alguna vez llamará algo o no? Hay muchos otros ejemplos, donde decidir si alguna función se llama alguna vez es mucho más costoso que simplemente incluirlo en el programa.public static void main(String[] args) {int counterexample = findCollatzConjectureCounterexample(); System.out.println(counterexample);}
<- ¿Está el código muerto de la llamada println? ¡Ni siquiera los humanos pueden resolver eso!Respuestas:
El problema del código muerto está relacionado con el problema de detención .
Alan Turing demostró que es imposible escribir un algoritmo general que recibirá un programa y podrá decidir si ese programa se detiene para todas las entradas. Es posible que pueda escribir dicho algoritmo para tipos específicos de programas, pero no para todos los programas.
¿Cómo se relaciona esto con el código muerto?
El problema de detención se puede reducir al problema de encontrar código muerto. Es decir, si encuentra un algoritmo que puede detectar código muerto en cualquier programa, puede usar ese algoritmo para probar si algún programa se detendrá. Como se ha demostrado que eso es imposible, se deduce que escribir un algoritmo para código muerto también es imposible.
¿Cómo transfieres un algoritmo para código muerto a un algoritmo para el problema de detención?
Simple: agrega una línea de código después del final del programa que desea verificar para detener. Si su detector de código muerto detecta que esta línea está muerta, entonces sabe que el programa no se detiene. Si no es así, entonces sabe que su programa se detiene (llega a la última línea y luego a la línea de código agregada).
Los compiladores usualmente verifican que las cosas que pueden ser probadas en tiempo de compilación estén muertas. Por ejemplo, bloques que dependen de condiciones que se pueden determinar como falsas en tiempo de compilación. O cualquier declaración después de un
return
(dentro del mismo alcance).Estos son casos específicos y, por lo tanto, es posible escribir un algoritmo para ellos. Es posible escribir algoritmos para casos más complicados (como un algoritmo que verifica si una condición es sintácticamente una contradicción y, por lo tanto, siempre será falsa), pero aún así, eso no cubriría todos los casos posibles.
fuente
256^(2^64)
estados esO(1)
, por lo que la detección de código muerto se puede hacer en tiempo polinómico.¡Bien, tomemos la prueba clásica de la indecidibilidad del problema de detención y cambiemos el detector de detención a un detector de código muerto!
Programa C #
Si
YourVendor.Compiler.HasDeadCode(quine_text)
vuelvefalse
, entonces la líneaSystem.Console.WriteLn("Dead code!");
no será nunca ejecutada, por lo que este programa realmente no tiene código muerto, y el detector estaba mal.Pero si regresa
true
, entonces la líneaSystem.Console.WriteLn("Dead code!");
se ejecutará, y como no hay más código en el programa, no hay ningún código muerto, por lo que nuevamente, el detector estaba equivocado.Entonces, ahí lo tiene, un detector de código muerto que devuelve solo "Hay código muerto" o "No hay código muerto" a veces debe dar respuestas incorrectas.
fuente
Si el problema de detención es demasiado oscuro, piénselo de esta manera.
Tome un problema matemático que se cree que es verdadero para todos los enteros positivos n , pero no se ha demostrado que sea cierto para todos los n . Un buen ejemplo sería la conjetura de Goldbach , de que cualquier entero positivo incluso mayor que dos puede representarse por la suma de dos números primos. Luego (con una biblioteca bigint apropiada) ejecute este programa (sigue el pseudocódigo):
La implementación de
isGoldbachsConjectureTrueFor()
se deja como un ejercicio para el lector, pero para este propósito podría ser una simple iteración sobre todos los números primos menores quen
Ahora, lógicamente, lo anterior debe ser el equivalente de:
(es decir, un bucle infinito) o
como la conjetura de Goldbach debe ser verdadera o no verdadera. Si un compilador siempre pudiera eliminar el código muerto, definitivamente habría un código muerto para eliminar aquí en cualquier caso. Sin embargo, al hacerlo al menos su compilador necesitaría resolver problemas arbitrariamente difíciles. Podríamos proporcionar problemas demostrablemente difíciles que tendría que resolver (por ejemplo, problemas de NP completo) para determinar qué bit de código eliminar. Por ejemplo, si tomamos este programa:
sabemos que el programa imprimirá "Valor SHA encontrado" o "Valor SHA no encontrado" (puntos de bonificación si puede decirme cuál es el verdadero). Sin embargo, para que un compilador pueda optimizar razonablemente eso tomaría del orden de 2 ^ 2048 iteraciones. De hecho, sería una gran optimización, ya que predigo que el programa anterior se ejecutará (o podría) hasta la muerte por calor del universo en lugar de imprimir cualquier cosa sin optimización.
fuente
sha256
devuelva una matriz de bytes y las matrices de bytes no se comparan con cadenas iguales en su idioma.Implementation of isGoldbachsConjectureTrueFor() is left as an exercise for the reader
Esto me hizo reír.No sé si C ++ o Java tienen una
Eval
función de tipo, pero muchos lenguajes le permiten llamar a los métodos por su nombre . Considere el siguiente (inventado) ejemplo de VBA.El nombre del método a llamar es imposible de saber hasta el tiempo de ejecución. Por lo tanto, por definición, el compilador no puede saber con absoluta certeza que nunca se llama a un método en particular.
En realidad, dado el ejemplo de llamar a un método por su nombre, la lógica de ramificación ni siquiera es necesaria. Simplemente diciendo
Es más de lo que el compilador puede determinar. Cuando se compila el código, todo lo que el compilador sabe es que cierto valor de cadena se pasa a ese método. No verifica si ese método existe hasta el tiempo de ejecución. Si el método no se llama a otra parte, a través de métodos más normales, un intento de encontrar métodos muertos puede devolver falsos positivos. El mismo problema existe en cualquier lenguaje que permita que se invoque el código mediante reflexión.
fuente
Los compiladores avanzados pueden detectar y eliminar el código muerto incondicional.
Pero también hay un código muerto condicional. Es un código que no se puede conocer en el momento de la compilación y solo se puede detectar durante el tiempo de ejecución. Por ejemplo, un software puede ser configurable para incluir o excluir ciertas características dependiendo de la preferencia del usuario, haciendo que ciertas secciones de código parezcan inactivas en escenarios particulares. Eso no es ser un verdadero código muerto.
Existen herramientas específicas que pueden hacer pruebas, resolver dependencias, eliminar el código muerto condicional y recombinar el código útil en tiempo de ejecución para mayor eficiencia. Esto se llama eliminación dinámica de código muerto. Pero como puede ver, está más allá del alcance de los compiladores.
fuente
Un simple ejemplo:
Ahora suponga que el puerto 0x100 está diseñado para devolver solo 0 o 1. En ese caso, el compilador no puede darse cuenta de que el
else
bloque nunca se ejecutará.Sin embargo, en este ejemplo básico:
Aquí el compilador puede calcular el
else
bloque es un código muerto. Por lo tanto, el compilador puede advertir sobre el código muerto solo si tiene suficientes datos para descubrir el código muerto y también debe saber cómo aplicar esos datos para averiguar si el bloque dado es un código muerto.EDITAR
A veces los datos simplemente no están disponibles en el momento de la compilación:
Mientras compila a.cpp, el compilador no puede saber que
boolMethod
siempre regresatrue
.fuente
El compilador siempre carecerá de información contextual. Por ejemplo, es posible que sepa que un valor doble nunca es 2, porque esa es una característica de la función matemática que utiliza desde una biblioteca. El compilador ni siquiera ve el código en la biblioteca, y nunca puede conocer todas las características de todas las funciones matemáticas y detectar todas las formas complicadas y complicadas de implementarlas.
fuente
El compilador no necesariamente ve todo el programa. Podría tener un programa que llame a una biblioteca compartida, que vuelve a llamar a una función en mi programa que no se llama directamente.
Por lo tanto, una función que está muerta con respecto a la biblioteca con la que se compila podría cobrar vida si esa biblioteca se cambiara en tiempo de ejecución.
fuente
Si un compilador pudiera eliminar todo el código muerto con precisión, se lo llamaría intérprete .
Considere este escenario simple:
my_func()
puede contener código arbitrario y para que el compilador determine si devuelve verdadero o falso, tendrá que ejecutar el código o hacer algo que sea funcionalmente equivalente a ejecutar el código.La idea de un compilador es que solo realiza un análisis parcial del código, simplificando así el trabajo de un entorno de ejecución separado. Si realiza un análisis completo, ya no es un compilador.
Si considera el compilador como una función
c()
, wherec(source)=compiled code
, y el entorno de ejecución comor()
, wherer(compiled code)=program output
, para determinar la salida de cualquier código fuente, debe calcular el valorr(c(source code))
. Si el cálculoc()
requiere el conocimiento del valor der(c())
cualquier entrada, no hay necesidad de un separador()
yc()
: puede derivar una funcióni()
dec()
tal manerai(source)=program output
.fuente
Otros han comentado sobre el problema de detención y demás. Estos generalmente se aplican a porciones de funciones. Sin embargo, puede ser difícil / imposible saber si se usa o no un tipo completo (clase / etc.).
En .NET / Java / JavaScript y otros entornos en tiempo de ejecución no hay nada que impida que los tipos se carguen a través de la reflexión. Esto es popular con los marcos de inyección de dependencia, y es aún más difícil de razonar frente a la deserialización o la carga dinámica del módulo.
El compilador no puede saber si esos tipos se cargarían. Sus nombres podrían provenir de archivos de configuración externos en tiempo de ejecución.
Es posible que desee buscar sacudidas de árboles, que es un término común para las herramientas que intentan eliminar de forma segura subgrafías de código no utilizadas.
fuente
Tomar una función
¿Puedes demostrar que
actnumber
nunca será2
así queAction2()
nunca se llama ...?fuente
Action2()
nunca se llamará", es imposible probar la afirmación en la práctica: un compilador no puede resolverla por completo . La diferencia es como 'existe un número X' vs. 'podemos escribir el número X en decimal'. Para algunas X, lo último nunca sucederá aunque lo primero sea cierto.actnumber==2
. Esta respuesta simplemente afirma que es difícil sin siquiera indicar una complejidad.No estoy de acuerdo con el problema de detención. No llamaría muerto a ese código, aunque en realidad nunca se alcanzará.
En cambio, consideremos:
(Ignore el tipo y los errores de desbordamiento) ¿Código muerto?
fuente
Mira este ejemplo:
El compilador no puede saber que un int solo puede ser par o impar. Por lo tanto, el compilador debe poder comprender la semántica de su código. ¿Cómo se debe implementar esto? El compilador no puede garantizar que el rendimiento más bajo nunca se ejecutará. Por lo tanto, el compilador no puede detectar el código muerto.
fuente
return i%2==0;
.i % 2 == 0
yi % 2 != 0
ni siquiera requiere razonamiento sobre el valor de un módulo entero una constante (que todavía es fácil de hacer), solo requiere la eliminación de la subexpresión común y el principio general (canonicalización, incluso) queif (cond) foo; if (!cond) bar;
se puede simplificarif (cond) foo; else bar;
. Por supuesto, "comprender la semántica" es un problema muy difícil, pero esta publicación no muestra que lo sea, ni muestra que resolver este problema es necesario para la detección de código muerto.i % 2
y la extraerá en una variable temporal. Luego reconocerá que las dosif
declaraciones son mutuamente excluyentes y pueden escribirse comoif(a==0)...else...
, y luego detectará que todas las rutas de ejecución posibles pasan por las dos primerasreturn
declaraciones y, por lo tanto, la tercerareturn
declaración es código muerto. (Un buen compilador de optimización es aún más agresivo: GCC convirtió mi código de prueba en un par de operaciones de manipulación de bits).if (availableMemory()<0) then {dead code}
.{dead code}
parte. GCC descubre esto al demostrar que hay un desbordamiento de entero con signo inevitable. Todo el código en ese arco en el gráfico de ejecución es, por lo tanto, código muerto. GCC puede incluso eliminar la rama condicional que conduce a ese arco.