Generacion de Codigo Intermedio

18
Juliodel 2012 Ingeniería informática VII | UNIVERSID AD NACIONAL DE TRUJILLO GENERACIÓN DE CÓDIGO INTERMEDIO INTEGRANTES Sánchez Muñoz Jean Carlos Novoa Ruiz Pedro Luis

Transcript of Generacion de Codigo Intermedio

Page 1: Generacion de Codigo Intermedio

|

universidad nacional de trujillo

GENERACIÓN DE CÓDIGO INTERMEDIO

INTEGRANTES

Sánchez Muñoz Jean Carlos

Novoa Ruiz Pedro Luis

Page 2: Generacion de Codigo Intermedio

GENERACIÓN DE CÓDIGO INTERMEDIO

1. Introducción

El proceso de la compilación se desglosa en dos partes: la parte que depende solo del lenguaje fuente (etapa inicial o front-end) y la parte que depende solo del lenguaje objeto (etapa final o back-end).

La etapa inicial traduce un programa fuente a una representación intermedia a partir de la cual la etapa final genera el código objeto. De esta forma, los detalles que tienen que ver con las características del lenguaje objeto (código ensamblador, código maquina absoluto o relocalizable, etc.), la arquitectura de la maquina (número de registros, modos de direccionamiento, tamaño de los tipos de datos, memoria cache, etc.), el entorno de ejecución (estructura de registros y memoria de la máquina donde se va a ejecutar el programa) y el sistema operativo se engloban en la etapa final y se aíslan del resto.

El código intermedio es un código abstracto independiente de la máquina para la que se generará el código objeto. El código intermedio ha de cumplir dos requisitos importantes: ser fácil de producir a partir del análisis sintáctico, y ser fácil de traducir al lenguaje objeto.

Con una representación intermedia bien definida, un compilador para el lenguaje I y la maquina J puede ser construido combinando el front-end para el lenguaje I con el back-end de la maquina J. De esta manera se pueden construir m*n compiladores escribiendo únicamente m front-ends y n back-ends.

2. Lenguajes Intermedios

Un lenguaje intermedio es el lenguaje de una máquina abstracta diseñada para ayudar en el análisis de los programas de computadora. El término viene de su uso en los compiladores, donde un compilador primero traduce el código fuente de un programa, en una forma más apropiada para las transformaciones de mejora del código (forma usualmente llamada bytecode), como un paso intermedio antes de generar el archivo objeto o el código máquina para una máquina específica.

2.1. Características

Su principal ventaja es la portabilidad, pues el mismo código puede ser ejecutado en diferentes plataformas y arquitecturas.

Esta ventaja la tiene también los lenguajes interpretados, aunque generalmente con mejor rendimiento. Por esto, muchos lenguajes interpretados se compilan a bytecode y después son ejecutados por un intérprete de bytecode.

2.2. Código intermedio en las fases de un Compilador

Page 3: Generacion de Codigo Intermedio

El siguiente gráfico muestra las dos etapas y como se relacionan entre sí a través de la representación intermedia.

3. Tipos de Lenguajes Intermedios

Hay tres tipos de lenguaje intermedio los cuales son:

3.1. Tipo 1

Es una representación más abstracta y uniforme que un lenguaje máquina concreto. Su misión es descomponer las expresiones complejas en binarias y las sentencias complejas en sentencias simples.

a) Ventajas

Permite una fase de análisis (análisis semántico) independiente de la máquina.

Se pueden realizar optimizaciones sobre el código intermedio (Las complejas rutinas de optimización son independientes de la máquina.

b) Desventajas

Perdida de eficiencia (no permite una compilación de una sola pasada).

Introduce en el compilador una nueva fase de traducción.

Page 4: Generacion de Codigo Intermedio

3.2. Tipo 2

Tipo de lenguajes intermedios:

Árbol sintáctico.

Árbol sintáctico abstracto (todos los nodos del árbol representan símbolos terminales, los nodos hijos son operandos y los nodos internos son operadores).

Grafo dirigido a cíclico (GDA).

Notación posfija.

3.3. Tipo 3

Tripletas Cuartetos

4. Generadores de Código

Se describen a continuación:

4.1. Características

- La generación de código es un proceso que toma un código fuente y genera un código objeto de igual semántica (generalmente en un nivel de abstracción menor).

- En la estructura general de un compilador, este proceso acostumbra a aparecer en dos contextos.

o Generación de Código Intermedio (front-end).o Generación de Código Destino (back-end).

- El código intermedio de un compilador suele ser código del lenguaje de una máquina abstracta.

- Puede ser interna (gcc) o externa al compilador (Java).

o La representación externa más utilizada está basada en máquina de pila (JVM, LLVM, CLI).

o Es en la representación interna donde más aparece la representación basada en código de tres direcciones.

4.2. Tareas de un generador de Código

Cualquier proceso de generación de código, debe resolver los siguientes problemas:

- Selección de instrucciones

Page 5: Generacion de Codigo Intermedio

Encontrar la secuencia de instrucciones del lenguaje destino con igual semántica que las construcciones del lenguaje de entrada.

- Selección de direcciones de almacenamiento

En función del código destino, deberá identificarse para cada símbolo.

o Un modo de almacenamiento (dinámico o estático).o Una dirección de memoria (absoluta o relativa).o Un esquema de traducción de estructuras de alto nivel a su

traducción a bajo nivel.

- Asignación de registros

Si el código destino está basado en un lenguaje que utiliza (un número limitado de) registros, se deberá encontrar un modo de distribuir éstos.

5. Código de Tres Direcciones

5.1. Características

Son las siguientes:

- Se compone de instrucciones que contienen tres (o menos) direcciones, dos para los operandos y una para el resultado.

- Los operandos y el resultado pueden ser nombres, o variables temporales generadas por el compilador. Los operandos pueden también ser constantes.

- Las instrucciones pueden estar marcadas por etiquetas simbólicas (representan un índice dentro del conjunto de instrucciones).

- Tiene instrucciones de control de flujo.

5.2. Casos Comunes

5.2.1. Instrucciones de Asignación

x := y op z (op es un operador binario, aritmético o lógico).x :=op y (op es un operador unario, menos conversión).x := y

a) Asignaciones indexadas

x := y [ i ] x [ i] := y

Page 6: Generacion de Codigo Intermedio

b) Apuntadores

x :=¿ y (asigna la dirección de y).x :=¿ y (y es un apuntador).¿ x := y (x es apuntador).

5.2.2. Saltos

a) Incondicional

goto L

b) Condicional

if x rel-op y goto L

5.2.3. Llamada a procedimientos y retorno

param x1

param x2

...param xn

call p nreturn y

6. Implementaciones de Códigos de Tres DireccionesUna proposición de tres direcciones es una forma abstracta de código intermedio. En un compilador estas proposiciones se pueden implementar como registros con campos para el operador y los operandos.Las implementaciones pueden ser:

6.1. Tripletas

Para evitar tener que introducir nombres temporales en la tabla de símbolos, se hace referencia a un valor temporal según la posición de la proposición que lo calcula. Las propias instrucciones representan el valor del nombre temporal. La implementación se hace mediante registros de solo tres campos (op, arg1, arg2).

op: operador

operador arg1 arg2

(0) * 2 X

(1) + (0) 10

Page 7: Generacion de Codigo Intermedio

arg1: argumento 1 ó operando 1.arg2: argumento 2 ó operando 2.

En la notación de tripletes se necesita menor espacio y el compilador no necesita generar los nombres temporales. Sin embargo, en esta notación, trasladar una proposición que defina un valor temporal exige que se modifiquen todas las referencias a esa proposición. Lo cual supone un inconveniente a la hora de optimizar el código, pues a menudo es necesario cambiar proposiciones de lugar.

Una forma de solucionar esto consiste en listar las posiciones a las tripletas en lugar de listar las tripletas mismas. De esta manera, un optimizador podría mover una instrucción reordenando la lista, sin tener que mover las tripletas en sí.

6.2. Cuádruplas

Una cuádrupla es una estructura tipo registro con cuatro campos que se llaman (op, result, arg1, arg2).

Op: operadorarg1: argumento 1 ó operando 1.arg2: argumento 2 ó operando 2.result: resultado de la operación.

Por ejemplo, la proposición de tres direcciones x = y + z se podría representar mediante la cuádrupla:

cuadrupla ( ADD , x , y , z )Los contenidos de los campos Argumento1, Argumento2 y Resultado son

generalmente punteros a la Tabla de Símbolos.

Las proposiciones con operadores unarios no usan el arg2. Los campos que no se usan se dejan vacíos o un valor NULL. Como se necesitan cuatro campos se le llama representación mediante cuádruplas.

7. Código intermedio como un atributo sintetizado

El código intermedio puede ser considerado como un atributo sintetizado.

op arg1 arg 2 result

(0) * 2 X T1

(1) + T1 10 T2

Page 8: Generacion de Codigo Intermedio

El código intermedio es visto como una cadena de caracteres y se puede diseñar un esquema de traducción dirigido por la sintaxis (ETDS) que genere dicho código al recorrer el árbol de análisis sintáctico en orden postfijo.

Consideremos como ejemplo la siguiente gramática simplificada que genera expresiones aritméticas. Dada la expresión E → E1+¿ E2¿ se calcula el valor del no-terminal E en un nombre temporal. Por el momento, se crea un nuevo nombre cada vez que se necesita. Más tarde veremos un sencillo método para reutilizar los nombres temporales.

Cada no-terminal tiene dos atributos:

- E . lugar, nombre temporal que contendrá el valor E ,(t 1 ,t 2 ,…).- E . cod, serie de todas las proposiciones de código de 3-direcciones que

calculan E.

Ambos atributos sintetizados. La función de nuevotemp () genera nombres distintos t 1 , t 2 , … cada vez que es llamada. Las llaves indican una instrucción de 3-direcciones. El símbolo // representa la concatenación de los trozos de código.

Ejemplo:

Expresión: x=x+3+4

t 1=x+3t 2=t 1+4x=t 2

Page 9: Generacion de Codigo Intermedio

7.1. Reutilización de los nombres temporales

Hasta ahora se ha supuesto que la función nuevotemp () genera un nombre temporal cada vez que se necesita un temporal. Sin embargo, los nombres temporales utilizados para guardar valores intermedios de los cálculos de expresiones tienen que ser introducidos en la tabla de símbolos para guardar sus valores con el consiguiente aumento de espacio.

Los nombres temporales se pueden reutilizar mediante un sencillo método que consiste en llevar una cuenta q, iniciada a cero, de variables temporales.Cuando se utilice un nombre temporal como operando hay que decrementar el valor de c es 1. Siempre que se genere un nuevo nombre temporal se usa el valor del contador sprintf (simbolo ,' ' t %d , c) y se incrementa el valor de c en 1. Consideremos el siguiente ejemplo x=a∗b+c∗d−e∗f

Page 10: Generacion de Codigo Intermedio

8. OPTIMIZACIÓN DE CÓDIGO INTERMEDIOLa optimización de código intermedio se puede realizar:

nivel local: sólo utilizan la información de un bloque básico para realizar la optimización.

nivel global: que usan información de varios bloques básicos.

El termino optimización de código es inadecuado ya que no se garantiza el obtener, en el sentido matemático, el mejor código posible atendiendo a maximizar o minimizar una función objetivo (tiempo de ejecución y espacio). El termino de mejora de código sería más apropiado que el de optimización.

Nos concentraremos básicamente en la optimización de código de tres direcciones, puesto que son siempre transportables a cualquier etapa final, son optimizaciones independientes de la máquina.

La mayoría de los programas emplean el 90% del tiempo de ejecución en el 10% de su código. Lo más adecuado es identificar las partes del programa que se ejecutan más frecuentemente y tratar de que se ejecuten lo más eficientemente posible. En la práctica, son los lazos internos los mejores candidatos para realizar las transformaciones.

Page 11: Generacion de Codigo Intermedio

Además de la optimización a nivel de código intermedio, se puede reducir el tiempo de ejecución de un programa actuando a otros niveles: a nivel de código fuente y a nivel de código objeto.

Un bloque básico es una unidad fundamental de código. Es una secuencia de proposiciones donde el flujo de control entra en el principio del bloque y sale al final del bloque. Los bloques básicos pueden recibir el control desde más de un punto en el programa (se puede llegar desde varios sitios a una etiqueta) y el control puede salir desde más de una proposición (se podría ir a una etiqueta o seguir con la siguiente instrucción). Cuando aplicamos optimización dentro de un bloque básico sólo nos tenemos que preocupar sobre los efectos de la optimización en los valores de las variables a la entrada del bloque y los valores que tienen a la salida del bloque, que hande ser los mismos que en el código original sin transformar.

El algoritmo para particionar un programa en bloques se describe a continuación:

1 Encontrar todas las proposiciones que comienzan el principio de un bloque básico:

La primera sentencia del programa.

Cualquier proposición del programa que es el objetivo de un salto.

Cualquier proposición que sigue a una bifurcación.

2 Para cualquier proposición que comienza un bloque básico, el bloque consiste de esa proposición y de todas las siguientes hasta el principio del siguiente bloque o el final del programa.

Algunas definiciones previas:

Dada una proposición de 3-direcciones de la forma a = b op c decimos que la proposición referencia b y c y que define a.

Se dice que un nombre en un bloque básico vive al final del bloque si su valor es referenciado en otro bloque básico en el programa.

Se dice que un nombre está muerto si no es referenciado en el resto del programa.

Se presentan algunas de las transformaciones más útiles para mejorar el código. Son

Page 12: Generacion de Codigo Intermedio

transformaciones locales, se pueden realizar observando sólo las proposiciones de un bloque básico.

8.1 ELIMINACIÓN DE SUBEXPRESIONES COMUNES

Si una expresión se calcula más de una vez, se puede remplazar el cálculo de la segunda por el valor de la primera expresión. Consideremos el siguiente ejemplo del código (a). Vemos que la expresión t3 * t1 se calcula dos veces, por tanto podemos escribir el código de la figura (b):

t1 = 4 - 2t2 = t1 / 2t3 = a * t2t4 = t3 * t1t5 = t4 + bt6 = t3 * t1t7 = t6 + bc = t5 * t7

(a)

t1 = 4 - 2t2 = t1 / 2t3 = a * t2t4 = t3 * t1t5 = t4 + b

t6 = t4t7 = t6 + bc = t5 * t5

(b)

Eliminación de sub-expresiones comunes

Esto sólo es posible si los operandos que están implicados en el cálculo de la expresión no han modificado su valor en las proposiciones intermedias.

8.1 PROPAGACIÓN DE COPIAS

La propagación de copias considera las proposiciones de la forma a=b. Después de esta sentencia sabemos que a y b tienen el mismo valor, por tanto, podemos remplazar cada vez que aparezca a por b con la esperanza de que podamos remplazar todas las ocurrencias de a hasta que se convierta en un nombre muerto y se pueda entonces eliminar la proposición de copia.

A partir del código anterior podemos eliminar la proposición de copia t6=t4. Sustituimos t6 por t4. Véase figura (a). Ahora se puede ver que hay de nuevo una subexpresión común que puede ser eliminada, obteniendo el código que se muestra en (b). Y finalmente realizando otra vez propagación de copias, obtenemos (c):

t1 = 4 - 2t2 = t1 / 2t3 = a * t2t4 = t3 * t1t5 = t4 + bt6 = t4t7 = t4 + bc = t5 * t7(a)

t1 = 4 - 2t2 = t1 / 2t3 = a * t2t4 = t3 * t1t5 = t4 + bt6 = t4t7 = t5c = t5 * t7(b)

t1 = 4 - 2t2 = t1 / 2t3 = a * t2t4 = t3 * t1t5 = t4 + bt6 = t4t7 = t5c = t5 * t5(c)

Propagación de copias y eliminación de subexpresiones comunes

Vemos que el haber hecho una optimización puede dar lugar a que sea posible

Page 13: Generacion de Codigo Intermedio

aplicar nuevas optimizaciones.

8.1 ELIMINACIÓN DE CÓDIGO MUERTO

Podemos tener proposiciones que definen un nombre que nunca más es referenciado, está muerto. Estas proposiciones pueden entonces ser eliminadas. Dada la proposición a= b op c, se dice que es código muerto o inactivo si no es referenciada. En general, el código muerto aparece como consecuencia de la propagación de copias y es esto lo que hace que la técnica de propagación de copias sea tan útil. Veamos cómo aplicar esta técnica al ejemplo anterior en la figura (c).

Vemos que t6 y t7 no tienen ninguna referencia a partir de su definición, por tanto pueden ser eliminadas. Obtenemos el código que aparece en la siguiente figura . Se ha supuesto que todas las variables no-temporales están vivas, se hace referencia a ellas en el resto del programa.

t1 = 4 - 2t2 = t1 / 2

t3 = a * t2 t4 = t3 * t1 t5 = t4 + b c = t5 * t5

Eliminación de código muerto

Aunque es poco probable que el programador introduzca código muerto o inactivo, éste puede aparecer como resultado de transformaciones anteriores.

8.1 TRANSFORMACIONES ARITMÉTICAS

Se pueden hacer uso de transformaciones algebraicas simples para reducir la cantidad de computación transformando operaciones más costosas por otras menos costosas. Existen tres tipos de transformaciones algebraicas básicas.

8.5.1 Calculo previo de constantesSe trata de calcular a nivel de compilación el valor previo de constantes en vez de hacerlo en tiempo de ejecución que retardaría la ejecución de un programa. A esta optimización se le llama cálculo previo de constantes (en inglés constant folding). El código de la figura anterior puede ser mejorado dando lugar al código de la figura 7.10 (a). Haciendo una propagación de copias y eliminación de código muerto tenemos el código de la figura 7.10 (b). Realizando de nuevo un cálculo previo de constantes obtenemos 7.10(c). Y finalmente podemos hacer de nuevo la propagación de copias y la eliminación de código muerto, obteniendo el código de la figura 7.10 (d).

t1 =2t2 = t1 / 2

t3 = a * t2t4 = t3 * t1t5 = t4 + b

c = t5 * t5(a)

t2 = 2 / 2t3 = a * t2t4 = t3 * 2

Page 14: Generacion de Codigo Intermedio

t5 = t4 + bc = t5 * t5 (b)

t2 = 1t3 = a * t2t4 = t3 * 2t5 = t4 + bc = t5 * t5

(c)

t3 = a * 1t4 = t3 * 2

t5 = t4 + bc = t5 * t5(d)

Tabla 7.10: Cálculo previo de constantesHemos pasado de seis proposiciones a tener cuatro, eliminado una substracción y una división en el proceso.

8.5.2 Transformaciones algebraicasPodemos hacer uso de identidades algebraicas para simplificar el código.Las principales identidades son:

x+0=0+x=x ; x−0=x ; x∗1=1∗x=x ; x /1=xPartiendo de la figura 7.10 (d) podemos obtener el código de la figura 7.11(a). De nuevo si usamos propagación de copias y eliminación de código muerto obtenemos el código de la figura 7.11 (b):

t3 = at4 = t3 * 2t5 = t4 + bc = t5 * t5(a)

t4 = a * 2t5 = t4 + bc = t5 * t5(b)

Tabla 7.11: Identidades algebraicas

8.5.3 Reducción de intensidadEn la mayoría de las máquinas las operaciones de multiplicación y división son substancialmente más costosas que las operaciones de suma y resta. Y a su vez, las potencias son más costosas que las multiplicaciones y divisiones. Por tanto, siempre que sea posible es conveniente sustituir un operador más costoso por otro menos costoso. A esto se le conoce como reducción de la intensidad. Las identidades más comunes son:

x2=x∗x ;2∗X=x+x

En nuestro ejemplo de la figura 7.11 (b) podemos obtener el código 7.12Otra transformación típica es usar desplazamientos de bits cuando se divida o se multiplique por potencias de dos.

t4 = a + at5 = t4 + bc = t5 * t5

Tabla 7.12: Reducción de intensidad