El optimizador de GCC y la opción -O

Los compiladores hoy en día suelen venir cargados de optimizadores dispuestos a corregir cualquier código que hayamos escrito que tal vez no sea el perfecto, para mejorar su rendimiento en espacio o en tiempo. En este vídeo evaluamos cómo el conjunto de opciones de GCC -O, como -O0, -O1, -O2 y -O3, afectan al código ensamblador que se genera en función de cómo de bien hayamos escrito el código.

Las optimizaciones en compiladores son una herramienta fundamental para mejorar el rendimiento y la eficiencia de nuestros programas sin necesidad de complicarnos con detalles de bajo nivel. En el caso de GCC, el compilador para C, estas optimizaciones permiten que el código generado sea más rápido y ocupe menos espacio, gracias a transformaciones automáticas que el compilador aplica al traducir nuestro código fuente a código máquina.

Cuando hablamos de optimizaciones, nos referimos a las mejoras que el compilador introduce para que el programa funcione mejor, ya sea en tiempo de ejecución o en tamaño. GCC ofrece una gran cantidad de opciones para controlar estas optimizaciones, pero la forma más sencilla y común de activarlas es mediante la opción -O seguida de un nivel, como -O0, -O1, -O2 o -O3. Es importante destacar que esta letra es una O mayúscula, no un cero. Cada nivel aplica un conjunto diferente de optimizaciones, desde no aplicar ninguna (-O0) hasta aplicar las más agresivas (-O3).

Además, existen opciones específicas como -Os, que optimiza el código para que ocupe el menor espacio posible, y -Og, que busca un equilibrio para facilitar la depuración con herramientas como GDB, manteniendo ciertas optimizaciones pero sin complicar demasiado el código generado.

Un ejemplo muy ilustrativo de optimización que realiza GCC es la transformación de llamadas a printf en llamadas a puts cuando el formato es simple. Por ejemplo, si escribimos:

printf("Hola Mundo\n");

el compilador puede detectar que esta llamada es equivalente a:

puts("Hola Mundo");

y reemplazarla automáticamente. Esto es porque puts es una función más sencilla y eficiente: simplemente imprime la cadena y añade un salto de línea, mientras que printf es una función variádica que debe interpretar el formato, lo que implica un coste adicional. Esta sustitución hace que el programa sea más eficiente sin que tengamos que cambiar nuestro código.

Otra optimización interesante es la eliminación de código innecesario. Supongamos que tenemos una variable res a la que asignamos varios valores consecutivos, pero que nunca usamos esos valores intermedios:

int res = 5;
res = 8;
res = 4;
res = 3;
res = 2;
res = 6;
return res;

Si compilamos sin optimizaciones, el compilador generará instrucciones para todas esas asignaciones, aunque no tengan efecto visible. Sin embargo, al activar optimizaciones con -O2 o superior, GCC detecta que solo el último valor asignado a res es relevante para el resultado final y elimina las asignaciones anteriores, generando un código mucho más limpio y eficiente. En ensamblador, esto se traduce en mover directamente el valor final al registro de retorno y omitir las instrucciones superfluas.

Incluso estructuras como bucles pueden ser eliminadas si el compilador determina que no afectan al resultado. Por ejemplo, un bucle que asigna valores a una variable sin que esos valores se usen después puede desaparecer completamente en el código optimizado.

Estas optimizaciones automáticas nos permiten escribir código claro y sencillo, confiando en que el compilador se encargará de mejorar su rendimiento cuando sea posible. Sin embargo, es importante recordar que no debemos depender exclusivamente de ellas para garantizar el correcto funcionamiento del programa. En casos donde el comportamiento del código depende de efectos secundarios o interacciones con hardware, por ejemplo, puede ser necesario usar palabras clave como volatile para indicarle al compilador que no elimine ciertas operaciones.

En definitiva, las opciones de optimización de GCC nos ofrecen un control sencillo y potente para mejorar nuestros programas en C, desde transformar funciones para hacerlas más eficientes hasta eliminar código inútil, facilitando que nuestro software sea más rápido y compacto sin que tengamos que preocuparnos por los detalles más complejos.

Lista de reproducción
  1. 1
    Instalar CodeBlocks
    15 minutos
  2. 2
    Funciones y hola mundo
    17 minutos
  3. 3
    Variables y tipos de datos
    17 minutos
  4. 4
    Condicionales y operadores lógicos
    16 minutos
  5. 5
    Bucles
    11 minutos
  6. 6
    Punteros
    12 minutos
  7. 7
    Arrays
    14 minutos
  8. 8
    Estructuras
    12 minutos
  9. 9
    Otras construcciones de C
    9 minutos
  10. 10
    Memoria dinámica
    8 minutos
  11. 11
    El preprocesador (parte 1)
    10 minutos
  12. 12
    El preprocesador (parte 2)
    9 minutos
  13. 13
    Archivos de cabecera y múltiples .c (parte 1)
    8 minutos
  14. 14
    Archivos de cabecera y múltiples .c (parte 2)
    9 minutos
  15. 15
    C desde la línea de comandos (parte 1)
    8 minutos
  16. 16
    C desde la línea de comandos (parte 2)
    9 minutos
  17. 17
    Break y continue
    10 minutos
  18. 18
    Goto
    13 minutos
  19. 19
    Manipulación de bits
    15 minutos
  20. 20
    Máscaras de bit
    19 minutos
  21. 21
    Archivos (1): fopen y fclose
    13 minutos
  22. 22
    Archivos (2): leer con fgetc
    9 minutos
  23. 23
    Archivos (3): fseek y ftell
    11 minutos
  24. 24
    Archivos (4): leer con fgets
    9 minutos
  25. 25
    Archivos (5): fputc y fputs
    7 minutos
  26. 26
    Archivos (6): volcar en archivos con fwrite
    10 minutos
  27. 27
    Archivos (7): fread, fwrite y los arrays
    10 minutos
  28. 28
    Archivos (8): entrada estándar y salida estándar
    9 minutos
  29. 29
    Archivos (9): buffers
    14 minutos
  30. 30
    Archivos (y 10): otras funciones útiles con archivos
    5 minutos
  31. 31
    printf (1)
    18 minutos
  32. 32
    printf (parte 2)
    12 minutos
  33. 33
    scanf (parte 1)
    17 minutos
  34. 34
    scanf (parte 2)
    17 minutos
  35. 35
    fprintf, sprintf y snprintf
    8 minutos
  36. 36
    Tipos de datos opacos
    13 minutos
  37. 37
    Bibliotecas estáticas
    13 minutos
  38. 38
    Bibliotecas dinámicas
    15 minutos
  39. 39
    Más flags: i mayúscula (include), wall, werror, pedantic...
    12 minutos
  40. 40
    pkg-config
    12 minutos
  41. 41
    Make
    17 minutos
  42. 42
    GDB
    21 minutos
  43. 43
    Variables globales
    6 minutos
  44. 44
    extern
    9 minutos
  45. 45
    Funciones variádicas
    12 minutos
  46. 46
    El optimizador de GCC y la opción -O
    12 minutos
  47. 47
    Volatile
    6 minutos