El análisis de flujo de datos es una técnica para recopilar información sobre el conjunto posible de valores calculados en distintos puntos de un programa informático . Constituye la base de una amplia variedad de optimizaciones de compiladores y técnicas de verificación de programas. El grafo de flujo de control (GFC) de un programa se utiliza para determinar las partes del programa a las que podría propagarse un valor específico asignado a una variable. Los compiladores suelen utilizar la información recopilada para optimizar un programa. Un ejemplo clásico de análisis de flujo de datos es el alcance de definiciones . Otros análisis de flujo de datos de uso común incluyen el análisis de variables activas, expresiones disponibles, propagación de constantes y expresiones muy ocupadas, cada uno con una función específica en las fases de optimización del compilador.
Una forma sencilla de realizar un análisis de flujo de datos en programas consiste en establecer ecuaciones de flujo de datos para cada nodo del grafo de flujo de control y resolverlas calculando repetidamente la salida a partir de la entrada localmente en cada nodo hasta que todo el sistema se estabilice, es decir, alcance un punto fijo . La eficiencia y la precisión de este proceso se ven significativamente influenciadas por el diseño del marco de flujo de datos, incluyendo la dirección del análisis (hacia adelante o hacia atrás), el dominio de valores y la operación de unión utilizada para combinar información de múltiples rutas de control.Este enfoque general, también conocido como el método de Kildall , fue desarrollado por Gary Kildall mientras impartía clases en la Escuela Naval de Posgrado . [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] [ 8 ]
Principios básicos
El análisis de flujo de datos es el proceso de recopilar información sobre cómo se definen y utilizan las variables en el programa. Busca obtener información específica en cada punto de un procedimiento. Generalmente, basta con obtener esta información en los límites de los bloques básicos , ya que a partir de ahí es fácil calcular la información en los puntos dentro del bloque. En el análisis de flujo hacia adelante, el estado de salida de un bloque es función de su estado de entrada. Esta función es la composición de los efectos de las instrucciones dentro del bloque. El estado de entrada de un bloque es función de los estados de salida de sus predecesores. Esto da como resultado un conjunto de ecuaciones de flujo de datos:
Para cada bloque b:
En esto,es la función de transferencia del bloqueFunciona en el estado de entrada., lo que produce el estado de salida. La operación de unióncombina los estados de salida de los predecesoresde, lo que produce el estado de entrada de.
Tras resolver este conjunto de ecuaciones, los estados de entrada y/o salida de los bloques pueden utilizarse para derivar propiedades del programa en los límites de los bloques. La función de transferencia de cada instrucción, por separado, puede aplicarse para obtener información en un punto dentro de un bloque básico.
Cada tipo particular de análisis de flujo de datos tiene su propia función de transferencia y operación de unión específicas. Algunos problemas de flujo de datos requieren un análisis de flujo inverso. Este sigue el mismo procedimiento, con la diferencia de que la función de transferencia se aplica al estado de salida para obtener el estado de entrada, y la operación de unión actúa sobre los estados de entrada de los sucesores para obtener el estado de salida.
El punto de entrada (en el flujo directo) desempeña un papel importante: al no tener predecesores, su estado de entrada está bien definido al inicio del análisis. Por ejemplo, el conjunto de variables locales con valores conocidos está vacío. Si el grafo de flujo de control no contiene ciclos (no hubo bucles explícitos ni implícitos en el procedimiento), la resolución de las ecuaciones es sencilla. El grafo de flujo de control puede entonces ordenarse topológicamente ; al ejecutarse en este orden, los estados de entrada pueden calcularse al inicio de cada bloque, ya que todos los predecesores de ese bloque ya se han procesado, por lo que sus estados de salida están disponibles. Si el grafo de flujo de control contiene ciclos, se requiere un algoritmo más avanzado.
Un algoritmo iterativo
La forma más común de resolver las ecuaciones de flujo de datos es mediante un algoritmo iterativo. Este comienza con una aproximación del estado de entrada de cada bloque. A continuación, se calculan los estados de salida aplicando las funciones de transferencia a los estados de entrada. Posteriormente, se actualizan los estados de entrada mediante operaciones de unión. Estos dos últimos pasos se repiten hasta alcanzar el punto fijo : la situación en la que los estados de entrada (y, por consiguiente, los de salida) no cambian.
Un algoritmo básico para resolver ecuaciones de flujo de datos es el algoritmo iterativo round-robin :
- para i ← 1 a N
- inicializar el nodo i
- mientras ( los conjuntos aún están cambiando )
- para i ← 1 a N
- recalcular conjuntos en el nodo i
- para i ← 1 a N
Convergencia
Para que el método iterativo sea útil, debe alcanzar un punto fijo. Esto se puede garantizar imponiendo restricciones a la combinación del dominio de valores de los estados, las funciones de transferencia y la operación de unión.
El dominio de valores debe ser un orden parcial con altura finita (es decir, no hay cadenas ascendentes infinitas).<< ...). La combinación de la función de transferencia y la operación de unión debe ser monótona con respecto a este orden parcial. La monotonicidad garantiza que en cada iteración el valor se mantendrá igual o aumentará, mientras que la altura finita asegura que no puede crecer indefinidamente. Por lo tanto, finalmente llegaremos a una situación donde T(x) = x para todo x, que es el punto fijo.
El enfoque de la lista de tareas
Es fácil mejorar el algoritmo anterior al observar que el estado de entrada de un bloque no cambia si los estados de salida de sus predecesores permanecen sin cambios. Por lo tanto, introducimos una lista de trabajo : una lista de bloques que aún deben procesarse. Cada vez que cambia el estado de salida de un bloque, añadimos sus sucesores a la lista de trabajo. En cada iteración, se elimina un bloque de la lista de trabajo y se calcula su estado de salida. Si el estado de salida ha cambiado, se añaden los sucesores del bloque a la lista de trabajo. Para mayor eficiencia, un bloque no debe aparecer en la lista de trabajo más de una vez.
El algoritmo se inicia colocando bloques generadores de información en la lista de trabajo. Finaliza cuando la lista de trabajo está vacía.
Pedidos
La eficiencia de la resolución iterativa de ecuaciones de flujo de datos está influenciada por el orden en que se visitan los nodos locales. [ 9 ] Además, depende de si las ecuaciones de flujo de datos se utilizan para el análisis de flujo de datos hacia adelante o hacia atrás sobre el CFG. Intuitivamente, en un problema de flujo hacia adelante, sería más rápido si todos los predecesores de un bloque se hubieran procesado antes que el bloque mismo, ya que entonces la iteración utilizará la información más reciente. En ausencia de bucles, es posible ordenar los bloques de tal manera que los estados de salida correctos se calculen procesando cada bloque solo una vez.
A continuación, se analizan algunos órdenes de iteración para resolver ecuaciones de flujo de datos (un concepto relacionado con el orden de iteración de una CFG es el recorrido de un árbol ).
- Orden aleatorio : este orden de iteración no tiene en cuenta si las ecuaciones de flujo de datos resuelven un problema de flujo de datos hacia adelante o hacia atrás. Por lo tanto, su rendimiento es relativamente bajo en comparación con órdenes de iteración especializados.
- Iteración en postorden : Este es un orden de iteración típico para problemas de flujo de datos hacia atrás. En la iteración en postorden , un nodo se visita después de que todos sus nodos sucesores hayan sido visitados. Normalmente, la iteración en postorden se implementa con la estrategia de búsqueda en profundidad .
- Iteración en postorden inverso : Este es un orden de iteración típico para problemas de flujo de datos hacia adelante. En la iteración en postorden inverso , un nodo se visita antes que cualquiera de sus nodos sucesores, excepto cuando el sucesor es alcanzado por una arista de retorno. (Tenga en cuenta que la iteración en postorden inverso no es lo mismo que la iteración en preorden ).
Inicialización
El valor inicial de los estados de entrada es importante para obtener resultados correctos y precisos. Si los resultados se utilizan para optimizaciones del compilador, deben proporcionar información conservadora ; es decir, al aplicar la información, el programa no debe cambiar su semántica. La iteración del algoritmo de punto fijo tomará los valores en la dirección del elemento máximo. Por lo tanto, inicializar todos los bloques con el elemento máximo no es útil. Al menos un bloque comienza en un estado con un valor menor que el máximo. Los detalles dependen del problema de flujo de datos. Si el elemento mínimo representa información totalmente conservadora, los resultados pueden utilizarse de forma segura incluso durante la iteración del flujo de datos. Si representa la información más precisa, se debe alcanzar el punto fijo antes de poder aplicar los resultados.
Ejemplos
A continuación se presentan ejemplos de propiedades de programas informáticos que pueden calcularse mediante análisis de flujo de datos. Cabe destacar que las propiedades calculadas mediante este análisis suelen ser solo aproximaciones de las propiedades reales. Esto se debe a que el análisis de flujo de datos opera sobre la estructura sintáctica de la grafo de flujo de control (GFC) sin simular el flujo de control exacto del programa. Sin embargo, para que siga siendo útil en la práctica, un algoritmo de análisis de flujo de datos se diseña normalmente para calcular una aproximación superior o inferior de las propiedades reales del programa.
Análisis prospectivo
El análisis de definición de alcance calcula, para cada punto del programa, el conjunto de definiciones que potencialmente pueden alcanzar dicho punto.
si b == 4 entonces a = 5; demás a = 3; fin si si a < 4 entonces ... La definición de variable que se alcanza aen la línea 7 es el conjunto de asignaciones a = 5en la línea 2 y a = 3en la línea 4.
Análisis retrospectivo
El análisis de variables vivas calcula, para cada punto del programa, las variables que podrían leerse posteriormente antes de su siguiente actualización de escritura. El resultado se suele utilizar para la eliminación de código muerto , eliminando las instrucciones que asignan un valor a una variable cuyo valor no se utiliza posteriormente.
El estado inicial de un bloque es el conjunto de variables que están activas al comienzo del mismo. Contiene inicialmente todas las variables activas (contenidas) en el bloque, antes de que se aplique la función de transferencia y se calculen los valores reales contenidos. La función de transferencia de una instrucción se aplica eliminando las variables que se escriben dentro de este bloque (retirándolas del conjunto de variables activas). El estado final de un bloque es el conjunto de variables que están activas al final del bloque y se calcula mediante la unión de los estados iniciales de los bloques sucesores.
Código inicial:
Análisis retrospectivo:
El estado de entrada de b3 solo contiene b y d , ya que c ha sido escrito. El estado de salida de b1 es la unión de los estados de entrada de b2 y b3. La definición de c en b2 puede eliminarse, ya que c no está activa inmediatamente después de la instrucción.
La resolución de las ecuaciones de flujo de datos comienza con la inicialización de todos los estados de entrada y salida al conjunto vacío. La lista de trabajo se inicializa insertando el punto de salida (b3) en ella (típico para el flujo inverso). Su estado de entrada calculado difiere del anterior, por lo que se insertan sus predecesores b1 y b2 y el proceso continúa. El progreso se resume en la tabla siguiente.
Nótese que b1 se introdujo en la lista antes que b2, lo que obligó a procesar b1 dos veces (b1 se volvió a introducir como predecesor de b2). Si se hubiera insertado b2 antes que b1, se habría podido completar antes.
Inicializar con el conjunto vacío es una inicialización optimista: todas las variables comienzan como inactivas. Cabe destacar que los estados de salida no pueden disminuir de una iteración a la siguiente, aunque el estado de salida puede ser menor que el estado de entrada. Esto se evidencia en que, después de la primera iteración, el estado de salida solo puede cambiar si cambia el estado de entrada. Dado que el estado de entrada comienza como el conjunto vacío, solo puede crecer en iteraciones posteriores.
Otros enfoques
Varios compiladores modernos utilizan la forma estática de asignación única como método para el análisis de dependencias de variables. [ 10 ]
En 2002, Markus Mohnen describió un nuevo método de análisis de flujo de datos que no requiere la construcción explícita de un grafo de flujo de datos, [ 11 ] sino que se basa en la interpretación abstracta del programa y mantiene un conjunto de contadores de programa. En cada bifurcación condicional, ambos destinos se agregan al conjunto de trabajo. Cada ruta se sigue durante tantas instrucciones como sea posible (hasta el final del programa o hasta que haya entrado en un bucle sin cambios), y luego se elimina del conjunto y se recupera el siguiente contador de programa.
Se ha demostrado que una combinación de análisis de flujo de control y análisis de flujo de datos es útil y complementaria para identificar regiones de código fuente cohesivas que implementan funcionalidades de un sistema (por ejemplo, características , requisitos o casos de uso ). [ 12 ]
Clases especiales de problemas
Existen diversas clases especiales de problemas de flujo de datos que tienen soluciones eficientes o generales.
Problemas de vectores de bits
Los ejemplos anteriores son problemas en los que el valor del flujo de datos es un conjunto, por ejemplo, el conjunto de definiciones alcanzables (usando un bit para la posición de una definición en el programa) o el conjunto de variables activas. Estos conjuntos se pueden representar eficientemente como vectores de bits , donde cada bit representa la pertenencia a un conjunto de un elemento en particular. Usando esta representación, las funciones de unión y transferencia se pueden implementar como operaciones lógicas bit a bit. La operación de unión es típicamente la unión o la intersección, implementada mediante la disyunción lógica bit a bit y la conjunción lógica . La función de transferencia para cada bloque se puede descomponer en los llamados conjuntos de generación y eliminación .
Como ejemplo, en el análisis de variables vivas, la operación de unión es la unión. El conjunto de eliminación es el conjunto de variables que se escriben en un bloque, mientras que el conjunto de generación es el conjunto de variables que se leen sin haber sido escritas previamente. Las ecuaciones de flujo de datos se convierten en:
En operaciones lógicas, esto se lee como
out( b ) = 0 para s en succ( b ) salida( b ) = salida( b ) o entrada( s ) in( b ) = (out( b ) y no kill( b )) o gen( b )
Los problemas de flujo de datos que tienen conjuntos de valores de flujo de datos que pueden representarse como vectores de bits se denominan problemas de vectores de bits , problemas gen-kill o problemas localmente separables . [ 13 ] Dichos problemas tienen soluciones genéricas de tiempo polinomial. [ 14 ]
Además de los problemas de definiciones alcanzables y variables vivas mencionados anteriormente, los siguientes problemas son instancias de problemas de vectores de bits: [ 14 ]
- expresiones disponibles
- expresiones muy ocupadas
- cadenas de definición de uso
Problemas de IFDS
Los problemas interprocedimentales, finitos, distributivos, de subconjuntos o problemas IFDS son otra clase de problemas con una solución genérica de tiempo polinomial. [ 13 ] [ 15 ] Las soluciones a estos problemas proporcionan análisis de flujo de datos sensibles al contexto y al flujo.
Hay varias implementaciones de análisis de flujo de datos basados en IFDS para lenguajes de programación populares, por ejemplo en los marcos Soot [ 16 ] y WALA [ 17 ] para análisis de Java.
Todo problema de vector de bits es también un problema IFDS, pero existen varios problemas IFDS importantes que no son problemas de vector de bits, incluidas las variables verdaderamente activas y las variables posiblemente no inicializadas.
Sensibilidades
El análisis del flujo de datos suele ser independiente de la trayectoria, aunque es posible definir ecuaciones de flujo de datos que produzcan un análisis que sí tenga en cuenta la trayectoria.
- Un análisis sensible al flujo tiene en cuenta el orden de las instrucciones en un programa. Por ejemplo, un análisis de alias de punteros que no tenga en cuenta el flujo puede determinar que "las variables x e y pueden referirse a la misma ubicación", mientras que un análisis sensible al flujo puede determinar que "después de la instrucción 20, las variables x e y pueden referirse a la misma ubicación".
- Un análisis sensible a la ruta calcula diferentes piezas de información de análisis dependiendo de los predicados en las instrucciones de bifurcación condicional. Por ejemplo, si una bifurcación contiene una condición
x>0, entonces en la ruta de caída , el análisis asumiría quex<=0y en el destino de la bifurcación asumiría que efectivamentex>0se cumple. - Un análisis sensible al contexto es un análisis interprocedimental que considera el contexto de la llamada al analizar el destino de una llamada a función. En particular, al usar información de contexto, se puede volver al punto de llamada original, mientras que sin esa información, la información de análisis debe propagarse a todos los posibles puntos de llamada, lo que podría resultar en una pérdida de precisión.
Lista de análisis de flujo de datos
Véase también
Referencias
- ↑ Kildall, Gary Arlen (mayo de 1972). Optimización global de expresiones durante la compilación (tesis doctoral). Seattle, Washington, EE. UU.: Universidad de Washington , Grupo de Ciencias de la Computación. Tesis n.° 20506, Informe técnico n.° 72-06-02.
- ↑ Kildall, Gary Arlen (1973-10-01). "Un enfoque unificado para la optimización global de programas" (PDF) . Actas del 1er simposio anual ACM SIGACT-SIGPLAN sobre Principios de lenguajes de programación - POPL '73 . págs. 194–206 . doi : 10.1145/512927.512945 . hdl : 10945/42162 . S2CID 10219496. Archivado (PDF) del original el 29-06-2017 . Recuperado el 20-11-2006 . ()
- ↑ Rüthing, Oliver; Knoop, Jens; Steffen, Bernhard (31 de julio de 2003) [1999]. "Optimización: detección de igualdades de variables, combinando eficiencia con precisión" . En Cortesi, Agostino; Filé, Gilberto (eds.). Análisis estático: 6.º Simposio Internacional, SAS'99, Venecia, Italia, 22-24 de septiembre de 1999, Actas . Lecture Notes in Computer Science. Vol. 1694 ( ed. ilustrada). Springer. pp. 232-247 [233]. ISBN 9783540664598ISSN 0302-9743
- ↑ Huitt, Robert; Eubanks, Gordon ; Rolander, Thomas "Tom" Alan ; Laws, David; Michel, Howard E.; Halla, Brian; Wharton, John Harrison ; Berg, Brian; Su, Weilian; Kildall, Scott ; Kampe, Bill (2014-04-25). Laws, David (ed.). "Legacy of Gary Kildall: The CP/M IEEE Milestone Dedication" (PDF) (transcripción de video). Pacific Grove, California, EE. UU.: Computer History Museum . Número de referencia CHM: X7170.2014 . Recuperado el 19 de enero de 2020.
[…]
Eubanks
: […]
Gary
[…] era un inventor, era inventivo, hacía cosas. Su tesis doctoral demostró que el análisis de flujo global converge. […] Esta es una idea fundamental en la ciencia de la computación. […] Una vez tomé un curso de verano con un tipo llamado
Dhamdhere
[…] hablaron de optimización durante una semana y luego mostraron una diapositiva que decía: "El método de Kildall", esta es la verdadera historia. […] Es algo en lo que nadie piensa. […]
(33 páginas)
- ↑ Kildall, Gary A. (1973). "Un enfoque unificado para la optimización global de programas". Actas del 1er simposio anual ACM SIGACT-SIGPLAN sobre Principios de lenguajes de programación - POPL '73 . págs. 194–206 . doi : 10.1145/512927.512945 . hdl : 10945/42162 .
- ↑ Aho, Alfred V.; Lam, Monica S.; Sethi, Ravi; Ullman, Jeffrey D. (2006). Compiladores: Principios, técnicas y herramientas (2.ª ed.). Pearson. ISBN 978-0321486813.
- ^ Nielson, Fleming; Nielson, Hanne R.; Hankin, Chris (2005). Principios de análisis de programas. Saltador. ISBN 978-3540654100.
- ↑ Muchnick, Steven S. (1997). Diseño e implementación avanzados de compiladores. Morgan Kaufmann. ISBN 978-1558603202.
- ↑ Cooper, Keith D. ; Harvey, Timothy J.; Kennedy, Ken (26 de marzo de 2004) [noviembre de 2002]. "Análisis iterativo del flujo de datos, revisado" (PDF) . PLDI 2003 . ACM . TR04-432 . Recuperado el 1 de julio de 2017 .
- ↑ "Asignación única estática (con ejemplos relevantes)" . GeeksforGeeks . 2021-10-02 . Consultado el 2023-08-16 .
- ↑ Mohnen, Markus (2002). "Un enfoque sin grafos para el análisis del flujo de datos". Construcción de compiladores . Notas de clase en ciencias de la computación. Vol. 2304. págs. 185–213 . doi : 10.1007/3-540-45937-5_6 . ISBN 978-3-540-43369-9.
- ↑ Kuang, Hongyu; Mäder, Patrick; Hu, Hao; Ghabi, Achraf; Huang, LiGuo; Lü, Jian; Egyed, Alexander (2015-11-01). "¿Pueden las dependencias de datos de métodos respaldar la evaluación de la trazabilidad entre requisitos y código fuente?". Journal of Software: Evolution and Process . 27 (11): 838– 866. doi : 10.1002/smr.1736 . ISSN 2047-7481 . S2CID 39846438 .
- 1 2 Reps, Thomas; Horwitz, Susan; Sagiv, Mooly (1995). "Análisis preciso del flujo de datos interprocedimental mediante la alcanzabilidad de grafos". Actas del 22.º simposio ACM SIGPLAN-SIGACT sobre Principios de los lenguajes de programación - POPL '95 . Nueva York, Nueva York, EE. UU.: ACM Press . págs. 1, 49–61 . doi : 10.1145/199448.199462 . ISBN 0-89791692-1. S2CID 5955667 .
- 1 2 Knoop, Jens; Steffen, Bernhard ; Vollmer, Jürgen (1996-05-01). "Paralelismo gratuito: análisis de vectores de bits eficientes y óptimos para programas paralelos" . ACM Transactions on Programming Languages and Systems . 18 (3): 268– 299. doi : 10.1145/229542.229545 . ISSN 0164-0925 . S2CID 14123780 .
- ↑ Naeem, Nomair A.; Lhoták, Ondřej; Rodriguez, Jonathan (2010), "Extensiones prácticas al algoritmo IFDS", Compiler Construction , Lecture Notes in Computer Science, vol. 6011, Berlín/Heidelberg, Alemania: Springer Verlag , pp. 124–144 , doi : 10.1007/978-3-642-11970-5_8 , ISBN 978-3-64211969-9
- ↑ Bodden, Eric (2012). "Análisis de flujo de datos interprocedimental con IFDS/IDE y Soot". Actas del Taller Internacional ACM SIGPLAN sobre el Estado del Arte en el análisis de programas Java . Nueva York, Nueva York, EE. UU.: ACM Press . págs. 3–8 . doi : 10.1145/2259051.2259052 . ISBN 978-1-45031490-9. S2CID 3020481 .
- ↑ Rapoport, Marianna; Lhoták, Ondřej; Tip, Frank (2015). Análisis preciso del flujo de datos en presencia de llamadas a métodos correlacionadas . Simposio Internacional de Análisis Estático. Notas de clase en Ciencias de la Computación. Vol. 9291. Berlín/Heidelberg, Alemania: Springer Verlag . págs. 54–71 . doi : 10.1007/978-3-662-48288-9_4 . ISBN 978-3-66248287-2.
Lecturas adicionales
- Cooper, Keith D .; Torczon, Linda (2003) [2002-01-01]. Ingeniería de un compilador . Morgan Kaufmann . ISBN 978-1-55860-698-2.
- Muchnick, Steven Stanley (1997). Diseño e implementación avanzados de compiladores . Morgan Kaufmann Publishers . ISBN 978-1-55860-320-2.
- Hecht, Matthew S. (1977-05-03). Análisis de flujo de programas informáticos . Serie Lenguajes de Programación. Vol. 5. Elsevier North-Holland Inc. ISBN 978-0-44400210-5.
- Khedker, Uday P.; Sanyal, Amitabha; Karkare, Bageshri (2009). Análisis del flujo de datos: teoría y práctica . CRC Press ( Taylor and Francis Group ).
- Nielson, Flemming; Nielson, Hanne Riis ; Hankin, Chris (2005). Principios del análisis de programas . Springer Science+Business Media .
- Análisis del flujo de datos
- Optimizaciones del compilador