Skip to content

Clase 16 — Entornos de Ejecución, Optimización y Generación de Código Objeto

Resumen Ejecutivo

Última clase de la asignatura. Tres bloques:

  1. Entornos de ejecución (continuación): organización de la memoria en ejecución (código, datos estáticos, montículo y pila), el registro de activación, las secuencias de llamada y retorno, las estrategias de asignación de memoria (estática, dinámica por pila, dinámica por montículo) y el papel de la tabla de símbolos por ámbito.
  2. Fase de síntesis — Optimización + Generación de código objeto. Por qué y cómo se optimiza (subexpresiones comunes, plegado de constantes, eliminación de código muerto, extracción de invariantes de bucle, reducción de fuerza) y cómo se genera el código objeto (selección de instrucciones, asignación de registros, direcciones relativas). ⚠️ Este bloque completo NO entra en el examen — es contexto para entender el compilador.
  3. Realimentación de la actividad grupal + simulacro de examen con una gramática donde la sintaxis y la semántica interactúan de forma sutil.

El profesor recuerda que la fase de síntesis (código intermedio, optimización, código objeto) no cae en el examen. Lo que sí cae: análisis léxico/sintáctico/semántico y las acciones/comprobaciones semánticas sobre gramáticas. ⚠️ EXAMEN


Conceptos Clave

  • Organización de la memoria de un programa: código máquina + zona de variables globales/estáticas (cada vez menor por buenas prácticas) + montículo (crece hacia direcciones altas) + pila (crece a la inversa). Si la pila se llena → desbordamiento. ⚠️
  • Registro de activación: bloque de memoria con todo lo necesario para ejecutar una función: valores temporales, variables locales, estado de la máquina (registros guardados), dirección de retorno, enlace de acceso (ámbitos anidados) y espacio para parámetros y valor de retorno (estos últimos en la cima, para recuperarlos fácil al retornar). ⚠️
  • Pila vs. montículo: la pila se gestiona de forma continua (se desasigna al retornar); el montículo guarda datos dinámicos que persisten hasta liberarlos explícitamente → se fragmenta y necesita compactación / recolección de basura (Java, Python). Regla práctica: usar el montículo lo menos posible. ⚠️
  • Estrategias de asignación de memoria:
  • Estática: se reserva todo antes de ejecutar. No soporta recursividad (cada función tiene posiciones fijas) → prácticamente obsoleta. ⚠️
  • Dinámica por pila: la habitual. Se reserva el registro de activación al llamar la función; en recursión coexisten tantos registros como llamadas activas. ⚠️
  • Dinámica por montículo.
  • Secuencia de llamada / secuencia de retorno: una instrucción call de código intermedio se traduce a varias de código máquina: evaluar parámetros (eval, por valor o por referencia), crear el registro de activación, guardar el retorno, saltar; y al volver: guardar el resultado, restaurar el estado y liberar el registro. ⚠️
  • Tabla de símbolos por ámbito: se crea una por función/bloque; punteros entre tablas anidadas resuelven el problema del ámbito. La misma tabla del análisis sirve hasta la generación de código si guarda tipo, tamaño y dirección relativa. ⚠️ EXAMEN (concepto)

Desarrollo del Temario

1. Registro de activación y paso de parámetros

  • En el código intermedio, cuádruplas especiales: call (nombre de función), eval (cada parámetro, indicando si es por copia o por referencia) y return (valor de retorno si lo hay).
  • Por valor: se copia el valor del parámetro en el espacio del parámetro formal.
  • Por referencia: se mantiene una referencia (&x) a la dirección del parámetro del llamador.
  • Conversión a/desde código intermedio: pasar de código fuente a cuádruplas es más fácil, pero de cuádruplas a objeto es más difícil (hay que eliminar temporales); con tercetos ocurre lo contrario.

2. Optimización de código (NO entra en examen)

Objetivo: transformar el programa en otro equivalente pero más eficiente (menos tiempo de ejecución y/o menos memoria). El generador de código intermedio produce código correcto pero no óptimo. Estrategias vistas:

Estrategia Idea Ejemplo
Subexpresiones comunes Si una expresión se recalcula y sus variables no han cambiado, se reutiliza y*60 calculado dos veces → una sola
Plegado de constantes Evaluar en compilación expresiones de solo constantes 3+5 → 8
Código muerto Eliminar asignaciones cuyo valor se machaca sin usarse x=5; x=7; → x=7;
Invariantes de bucle Sacar fuera del bucle cálculos que no dependen de la iteración x=a*b dentro de un for → fuera
Reducción de fuerza Sustituir una operación cara por otra más barata (depende de la arquitectura) potencia → multiplicaciones

Por qué es difícil: el optimizador debe simular el flujo de ejecución (un grafo de flujo) para asegurar que una variable no se reasigna entre dos usos. Por eso muchos compiladores no optimizan. Java optimiza sobre el bytecode (capa extra de compilación).

Anécdota del profesor (su tesis): optimizando bytecode generado por un compilador de Java estándar logró ~20 % menos memoria y ~40 % menos tiempo de ejecución.

Reflexión: a veces no interesa comercialmente optimizar (más recursos consumidos → se venden más máquinas). Lo recomendable: buenas prácticas de programación para no depender del optimizador.

3. Generación de código objeto (NO entra en examen)

  • Un código intermedio (3 direcciones) genera más instrucciones de código objeto (un + se traduce en cargar registro, operar, copiar). Con tercetos salen prácticamente las mismas.
  • Asignación de registros: las variables muy usadas conviene mantenerlas en registros de la máquina (acceso más rápido que memoria); al final el resultado se deja en memoria.
  • Direcciones relativas: la tabla de símbolos guarda tipo, tamaño (bytes) y dirección relativa al inicio del bloque de la función (0, 4, 8…). La secuencia de llamada reserva ese espacio y asocia cada nombre a su celda.
  • Decisiones de diseño: selección de instrucciones, asignación de registros, administración de memoria y orden de evaluación.

4. Realimentación de la actividad grupal (sí relevante para examen) ⚠️ EXAMEN

  • Declaración antes de uso y unicidad por ámbito: al encontrar una declaración, mirar en la tabla de símbolos; si ya existe → error "variable ya declarada". Al usar una variable en asignación/expresión, comprobar que está declarada; si no → error "variable no declarada".
  • Comprobaciones semánticas anidadas, una condición a la vez: para una producción E → E + E hay que comprobar (1) la primera variable declarada, (2) la segunda declarada, (3) tipos iguales, (4) tipos compatibles con el operador. Mejor un error específico por condición que un único "error semántico". ⚠️ EXAMEN
  • Tipo error propagado: crear un tipo error que se propaga hacia arriba cuando una variable no está declarada, para que las operaciones que lo contengan fallen sin generar cascadas confusas.
  • Funciones reutilizables: una función "comprobar compatibilidad con el operador" sirve para suma, resta, lógicas, etc. (en el examen hay que detallar las comprobaciones o explicar muy bien la función; no vale "aquí se hace no sé qué").
  • Expresiones lógicas / de comparación: retornan tipo lógico al padre (diferencia clave con las aritméticas, que retornan el tipo de los operandos).
  • NOT y menos unario: el NOT no entra en conflicto y no necesita truco de precedencia; el menos unario sí necesita renombrarse (token aparte) porque el - puede ser binario o unario.
  • Arrays: solo de tipos básicos (no arrays de arrays), índices enteros, tamaño 1–64. Importante: que un índice esté dentro del rango NO se puede comprobar en compilación (es un error de ejecución); solo se valida el tamaño en la declaración.
  • Estructuras de control: se pedía generar código solo para if, if-else y while (el for se excluyó por complejo).

5. Simulacro de examen — interacción sintaxis/semántica ⚠️ EXAMEN

Gramática recursiva por la izquierda con producciones tipo P → P a | b y Q → Q c | d, con restricciones semánticas. Lecciones clave:

  • A veces la sintaxis ya garantiza una restricción → no hay que implementarla por semántica (p. ej. "nº de B ≤ nº de D" si la gramática solo permite un B y un D: se cumple siempre). Hay que saber detectarlo y no arrastrar atributos inútiles (arrastrarlos no anula la pregunta, pero resta décimas por trabajo innecesario).
  • A veces la semántica prohíbe lo que la sintaxis permite → p. ej. la restricción "el último símbolo generado por Q debe ser D" obliga a no entrar nunca en la parte recursiva, dejando como única frase válida b a / b d. La acción semántica debe propagar el conteo y dar error si al llegar a la raíz P o Q han generado más de un símbolo.
  • Es legítimo reinterpretar/cuestionar una restricción en el examen siempre que se justifique bien el razonamiento.
  • Dato de organización: los profesores preparan ~14 modelos distintos de examen (distintos turnos), así que las gramáticas de los simulacros no caen tal cual.

Cierre de Curso

Última sesión de la asignatura (primera edición que imparte el profesor). Despedida y agradecimiento. El chat de la asignatura queda abierto hasta septiembre para dudas. Ofrecimiento de continuidad (TFG/máster/tesis) en el área de compiladores y bytecode. Reto declarado para el próximo curso: replanificar la asignatura para llegar a ver el compilador completo (incluida la fase de síntesis con más profundidad).


Preguntas de Autoevaluación

  1. Describe las cuatro zonas de la organización de memoria de un programa y en qué dirección crecen la pila y el montículo.
  2. ¿Qué contiene un registro de activación? ¿Por qué los parámetros y el valor de retorno van en la cima?
  3. ¿Por qué la asignación estática de memoria no soporta recursividad? ¿Qué estrategia se usa habitualmente en su lugar?
  4. Diferencia el paso de parámetros por valor y por referencia y cómo se reflejan en las cuádruplas eval.
  5. ¿Por qué la fase de optimización es difícil de implementar y muchos compiladores no la aplican?
  6. Enumera cinco estrategias de optimización y pon un ejemplo de cada una. (no entra en examen, pero ayuda a entender)
  7. Para una producción E → E + E, ¿qué comprobaciones semánticas anidadas hay que hacer y en qué orden?
  8. ¿Qué retorna al padre una expresión de comparación frente a una aritmética?
  9. ¿Por qué el índice de un array fuera de rango es un error de ejecución y no de compilación?
  10. Da un ejemplo de restricción semántica que la propia sintaxis ya garantiza y otra que la semántica prohíbe pese a que la sintaxis la permita.