Diseño de compiladores Gestión de la memoria / Generación de código ORGANIZACIÓN DE MEMORIA Organización de memoria Depende del tipo de lenguaje (declarativos, imperativos), del compilador y del sistema operativo Organización de memoria El primer responsable de la gestión de la memoria es el sistema operativo Al ejecutar un programa, el loader del sistema operativo, asigna la cantidad de memoria y carga el código a ejecutar en la zona de código Se debe detectar una posible colisión entre el heap y la pila Se puede abortar el programa o aumentar la cantidad de memoria asignada Organización de memoria En sistemas con paginación de memoria, el esquema anterior puede estar particionado y desparramado entre la memoria real y virtual IMPORTANTE: Toda referencia a una posición de memoria dentro del código debe ser relativa a la posición asignada al proceso por el sistema operativo Para esto se utiliza un registro base con la dirección a considerar Zona de código Contiene las instrucciones del programa a ejecutar Están escritas en código máquina Consiste en la traducción a código máquina de todos los procedimientos y funciones del programa El tamaño del código y su contenido se calculan en tiempo de compilación 1
Zona de código Zona de código El archivo ejecutable contiene todo este código Además contiene información relativa al tamaño de los diferentes bloques de memoria que se necesitan Esta información es utilizada para cargar el programa en memoria para su ejecución Normalmente el código se considera un solo bloque compacto Algunos compiladores, por el contrario particionan el código en partes denominadas overlays Son usados cuando la memoria disponible es menor que el tamaño del programa Zona de código Los overlays son secciones de código que se cargan en memoria de manera independiente A lo largo del tiempo, dos overlays pueden haber ocupado la misma zona de memoria El compilador debe decidir cómo agrupar las funciones en overlays de manera que no se realicen demasiadas cargas de código a lo largo de la ejecución Memoria estática Es la forma más sencilla de gestión de la memoria El compilador asigna una posición de memoria fija a cada variable que se utilice en el programa Era la única forma de gestión de memoria en los primeros compiladores (FORTRAN) Memoria estática Actualmente sólo se utiliza para almacenar las constantes y variables globales del programa La asignación de memoria se realiza en tiempo de compilación de forma consecutiva, teniendo en cuenta el tamaño de la variable a asignar La dirección asociada a cada variable es constante y relativa al comienzo del segmento de datos (DS) 2
Características del compilador No pueden manejar funciones recursivas, ya que estas requieren varias instancias de cada variable de la función Cada función tiene asignado un registro de activación que contiene los parámetros, variables locales y variables temporales de la función La memoria estática contiene las variables globales y la secuencia de registros de activación de cada función Memoria tipo pila Se introdujo para manejar lenguajes estructurados con llamadas recursivas Las funciones recursivas requieren manejar diferentes instancias del registro de activación, es decir, del conjunto de valores de sus variables en cada ejecución de la función Memoria tipo pila Estos registros de activación se almacenan en forma de pila, de manera que el registro de la cima de la pila corresponde a la función en ejecución Al terminar la ejecución de una función debe desapilarse el registro de activación y pasar el control a la función que se encuentre en la cima de la pila Memoria tipo pila Al traspasar el control de una función a otra es necesario almacenar el estado de la máquina, es decir, el conjunto de valores de los registros del procesador Se debe almacenar la dirección del comienzo del registro de activación de la función llamante (Frame Pointer) Esta información se añade al contenido del registro de activación de cada función 3
Memoria tipo pila Protocolo de llamada El procesador maneja dos registros especiales Stack Pointer (SP): dirección del tope de la pila Frame Pointer (FP): dirección base del registro de activación de la función activa Se reserva espacio para el valor devuelto Se almacena el valor de los parámetros que se pasan a la función llamada Se almacena el valor de la dirección de retorno Se almacena el estado de la máquina (incluye SP y FP) Se pasa el control a la función llamada Comienza la ejecución del código de la función llamada Protocolo de retorno Se coloca el valor devuelto en el espacio reservado (si existe) Se restaura el contenido del estado de la máquina. Esto modifica el valor de FP y SP Provoca la liberación de la memoria ocupada por el registro de activación de la función llamada Se restaura el valor de la dirección de retorno Protocolo de retorno Se devuelve el control a la función que llama Se almacena el valor devuelto en la variable local o temporal adecuada GENERACIÓN DE CÓDIGO 4
Introducción Introducción La fase final de nuestro modelo de compilador es el generador de código Toma por entrada una RI del fuente y produce su equivalente en código objeto Las técnicas que veremos son independientes de si se realiza o no optimización Los requerimientos que, tradicionalmente, se imponen en un generador de código son muy demandantes El código generado debe ser correcto y de alta calidad (debe hacer uso efectivo de los recursos de la máquina objeto) El generador, además, debe ser eficiente Introducción El problema de generar código óptimo es no decidible Son problemas NP completos No hay algoritmos polinomiales que resuelvan el problema En la práctica, se utilizan heurísticas que generan código bueno (aunque probablemente no óptimo) Aspectos generales Entrada al generador de código (RI) Programas objeto Administración de memoria Selección de instrucciones Adjudicación de registros Elección de orden de evaluación Entrada al generador de código La entrada consiste en la RI del código fuente, producida por el frontend, junto con la información de la tabla de símbolos La TS se utiliza para determinar las direcciones en tiempo de ejecución de los objetos de datos denotados por los nombres en la RI Asumimos que la RI es de suficiente bajo nivel con tipos de datos que se pueden mapear razonablemente a la arquitectura objeto Entrada al generador de código Asumimos que el chequeo de tipos ya fue realizado, con la adecuada inserción de operadores de cambio de tipos, y demás Asumimos que no hay errores semánticos obvios (Ej., intentar indizar un arreglo con un punto flotante) 5
Entrada al generador de código Esta fase se basa en la asunción de que la RI no tiene errores (aunque en algunos compiladores, el chequeo se realiza a la vez que la generación de código) Código objeto La salida del generador es el código o programa objeto, el cual puede tomar distintas formas: código máquina absoluto código máquina reubicable código assembler Código objeto Producir código máquina absoluto como salida conlleva la ventaja de que puede ser ubicado en un lugar fijo de memoria y ejecutado inmediatamente Código reubicable (también llamado módulo objeto) permite tener módulos que se compilan por separado. Luego se unen (linking) y se cargan Código objeto En este caso se gana mucha flexibilidad en función del trabajo que conlleva la unión y carga. Si la máquina objeto no se hace cargo de la reubicación, el compilador debe producir información explícita para el linker Código objeto Administración de memoria Finalmente, producir código assembler como salida facilita el proceso de compilación. Podemos generar instrucciones simbólicas y usar las macros de assembler como ayudas. Finalmente, facilita la optimización a mano del código generado. El precio es que aparece una etapa final de ensamblado Consiste en determinar la posición de memoria en la que los diferentes símbolos del programa almacenan la información Depende de la estrategia utilizada para la gestión de memoria, el mecanismo puede variar 6
Selección de instrucciones Selección de instrucciones La naturaleza del conjunto de instrucciones de la máquina objeto, determina la dificultad de la selección de instrucciones La uniformidad y completitud del conjunto de instrucciones son factores muy importantes. Si la máquina objeto no soporta cada tipo de datos de forma uniforme, entonces se necesitan estrategias alternativas Si no nos interesa la eficiencia, para cada tipo de sentencia en C3D podemos diseñar un esqueleto de código que muestra el código objeto a generar para esa construcción Ej.: x:=y+z MOV Y, R0 ADD Z, R0 MOV R0, X Esto genera código pobre Selección de instrucciones Por ejemplo: a := b + c d := a + e Se traduce a MOV b, R0 ADD c, R0 MOV R0, a redundante? MOV a, RO redundante? ADD e, R0 MOV R0, d Selección de instrucciones La calidad del código generado se mide en función de Tamaño Velocidad En función de la variedad de instrucciones uno puede seleccionar la instrucción más performante (ej. INC vs MOV - ADD - MOV). Para los casos no triviales, es un problema muy complejo. Asignación de registros Operar sobre registros es más rápido y eficiente que operar sobre memoria Por ello, la adjudicación eficiente de registros tiene un gran impacto en la performance Asignación de registros El uso de registros puede dividirse en dos subproblemas: Durante la reserva de registros (allocation), se seleccionan el conjunto de variables que vivirá en registros en un punto del programa. Durante la (posterior) asignación de registros (assignation), se elige el registro específico para cada variable. 7
Orden de evaluación El orden en que algunas computaciones se llevan a cabo puede afectar la eficiencia del código objeto Algunos ordenes requieren menos registros para almacenar valores intermedios Seleccionar el orden óptimo es también NPcompleto 8