Objetivos generales de la optimización de código


Arquitectura de Computadores Optimización Rendimiento IPC Ensamblador Desarrollo Software Ingeniería Compiladores Profiling

En Group4Layers, siempre que el cliente nos da libertad o realizamos proyectos internos, sustentamos nuestros desarrollos en varios pilares fundamentales, potenciando desde la calidad del código y las arquitecturas escalables hasta el mantenimiento y una cómoda trazabilidad, pero por supuesto, la eficiencia. Nuestras librerías y softwares liberados, como es ExImageInfo, se caracterizan por estos fundamentos. ¿El resultado? Pasan los años y seguimos siendo los creadores de la librería de procesamiento de imágenes más rápida (y con más soporte) del ecosistema Erlang/Elixir. Por algo se ha posicionado como la librería más popular, usada por individuos y empresas de todo el mundo. Artículos como este introducen en las complejas pero interesantes labores de optimización de código.

El objetivo fundamental a la hora de optimizar un programa es identificar aquellos eventos o regiones que impacten significativamente el rendimiento de un programa. Esto determinará cuales tendremos que considerar, capturar y evaluar. Sin embargo, también debemos tener en cuenta que nos interesan aquellos eventos que ocurren con mayor frecuencia y que pueden ser manipulados cómodamente a través de variaciones de código, siempre que no alteren las semánticas de nuestros programas.

Consideramos el tiempo de ejecución de un programa, definido como el producto del número de instrucciones ejecutadas, el promedio de número de ciclos por instrucción (CPI: cycles per instruction) y el periodo de reloj:

rendimiento y tiempo de ejecución

Ya que el periodo del reloj suele ser generalmente fijo (salvo configuraciones o sistemas con variaciones dinámicas), la optimización del código suele ser dependiente de la reducción del CPI y el número de instrucciones ejecutadas.

La reducción del número de instrucciones ejecutadas generalmente viene determinada por la reducción de código repetido e innecesario. Algunos ejemplos son almacenar y reutilizar el resultado de expresiones a ejecutar; extracción de código invariante que se encuentra interno a los bucles; o refactorización de funciones y métodos, moviendo regiones comunes a zonas reutilizables entre ellas. Generalmente el compilador es capaz de eliminar automáticamente tales redundancias en el análisis de código de alto nivel. Bien es cierto que hay ocasiones en las que el compilador generará más instrucciones de las necesarias al transformar código de alto nivel en código ensamblador, y es aquí donde se puede mejorar el rendimiento mediante la inserción de regiones especiales de código ensamblador. Esta práctica es común para regiones críticas para el rendimiento, generalmente en donde más elevado es el ratio de repetición (ejecución), como en el interior de los bucles anidados o en funciones y operadores constantemente utilizados.

Es de vital importancia disponer de métricas del rendimiento objetivas, como pueden ser contadores de número de instrucciones ejecutadas (retiradas), aunque es importante tener en cuenta que los procesadores son piezas muy complejas, y en la mayoría de ocasiones podremos estar antes un número estimado y no exacto (e.g. predictor de saltos y la especulación de saltos), pero suficiente para la mayoría de casos de optimización y propósitos más comunes. Algunos de los que hemos utilizado van desde softwares de código abierto como Linux Perf, GProf o Valgrind, hasta otros propietarios y específicos de un proveedor como AMD uProf, AMD CodeXL o Intel VTune, entre otros.

profiling con callgrind y kcachegrind

Analizando las instrucciones retiradas, repetición de regiones de código y cuellos de botella de un módulo enfocado al cómputo criptográfico. Usando KCacheGrind para inspeccionar un conjunto de trazas de ejecución ofrecidas por la herramienta Callgrind de Valgrind.

Por otro lado, la reducción del número promedio de CPI es algo más complejo. En primer lugar, los procesadores varían sus diseños hardware, y no solo los fabricantes sino cada arquitectura y serie llega a funcionar de maneras bastante diversas. Es más, el modelo y especificación teórica de cada procesador puede indicar un número CPI, pero las condiciones reales de ejecución pueden indicarnos otro bien distinto, generalmente inferior. Esto es debido a los eventos de tipo stall, es decir, casos en los que una instrucción no es ejecutada en el procesador, de forma temporal, debido a una serie de restricciones. El procesador se queda en una especie de espera, y la ejecución se ralentiza, disminuyendo el número máximo de instrucciones lanzadas en los ciclos posteriores. Los stalls son producidos principalmente por cuatro tipos de casos:

  • Fallos en la predicción de saltos (branch predictor)
  • Fallos en la indirección de llamadas y saltos durante la ejecución (branch target buffer, BTB), típicas en altos niveles de abstracción, punteros a funciones y herencia en POO.
  • Dependencias de datos, causadas por dependencias RAW (leer después de escribir) y con gran penalización en instrucciones con grandes latencias (e.g. punto flotante).
  • Carga/almacenado (load/store), involucrando todo tipo de fallos de cache, sobre todo cuando es necesario un acceso a memoria externa (fuera de chip).

Ahora que hemos introducido los grandes objetivos y problemas en la optimización de código, podemos centrarnos en estrategias en los dos grandes frentes aquí mencionados. Se abre un campo interesante de posibilidades para explotar, sin embargo, será mejor detallarlo en artículos futuros.

Este sitio web emplea cookies propias y de terceros para analizar el tráfico y ofrecerle una mejor experiencia. Al navegar o utilizar nuestros servicios el usuario está aceptando su uso.Más información.