Este artículo tiene como objetivo destacar la potencia de las optimizaciones de compiladores, centrándose en los compiladores Intel C++ , conocidos por su popularidad y uso generalizado.
Aspectos destacados: ¿Qué son las optimizaciones del compilador? | -En | Arquitectura dirigida | Optimización interprocedimental | -fno-aliasing | Informes de optimización del compilador
Cualquier compilador ejecuta una serie de pasos para convertir el código fuente de alto nivel al código de máquina de bajo nivel. Estos implican análisis léxico, análisis de sintaxis, análisis semántico, generación de código intermedio (o IR), optimización y generación de código.
Durante la fase de optimización, el compilador busca meticulosamente formas de transformar un programa, apuntando a una salida semánticamente equivalente que utilice menos recursos o se ejecute más rápidamente. Las técnicas empleadas en este proceso abarcan, entre otras , el plegado constante, la optimización de bucles, la inserción de funciones y la eliminación de códigos muertos .
Los desarrolladores pueden especificar un conjunto de indicadores del compilador durante el proceso de compilación, una práctica familiar para quienes usan opciones como " -g" o "-pg" con GCC para depurar y crear perfiles. A medida que avancemos, analizaremos indicadores de compilador similares que podemos usar al compilar nuestra aplicación con el compilador Intel C++. Estos podrían ayudarle a mejorar la eficiencia y el rendimiento de su código.
u(x,y,t) es la temperatura en el punto (x,y) en el tiempo t.
Básicamente tenemos una codificación C++ que realiza las iteraciones de Jacobi en cuadrículas de tamaños variables (que llamamos resoluciones). Básicamente, un tamaño de cuadrícula de 500 significa resolver una matriz de tamaño 500x500, y así sucesivamente.
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
MFLOP/s significa "Millones de operaciones de coma flotante por segundo". Es una unidad de medida utilizada para cuantificar el rendimiento de una computadora o procesador en términos de operaciones de punto flotante. Las operaciones de punto flotante implican cálculos matemáticos con números decimales o reales representados en formato de punto flotante.
Nota 1: Para proporcionar un resultado estable, ejecuto el ejecutable 5 veces para cada resolución y tomo el valor promedio de los valores MFLOP/s.
Nota 2: Es importante tener en cuenta que la optimización predeterminada en el compilador Intel C++ es -O2. Por lo tanto, es importante especificar -O0 al compilar el código fuente.
Estos son algunos de los indicadores del compilador más utilizados cuando se comienza con las optimizaciones del compilador. En un caso ideal, el rendimiento de Ofast > O3 > O2 > O1 > O0 . Sin embargo, esto no necesariamente sucede. Los puntos críticos de estas opciones son los siguientes:
-O1:
-O2:
-O3:
-Orápido:
Es claramente evidente que todas estas optimizaciones son mucho más rápidas que nuestro código base (con “-O0”). El tiempo de ejecución es entre 2 y 3 veces menor que el del caso base. ¿Qué pasa con los MFLOP/s?
En general, aunque sólo ligeramente, “-O3” tiene el mejor rendimiento.
Los indicadores adicionales utilizados por "- Ofast " (" -no-prec-div -fp-model fast=2 ") no proporcionan ninguna aceleración adicional.
La respuesta está en las banderas estratégicas del compilador. Experimentar con opciones como “ -xHost ” y, más precisamente, “ -xCORE-AVX512 ” puede permitirnos aprovechar todo el potencial de las capacidades de la máquina y adaptar las optimizaciones para un rendimiento óptimo.
-xHost:
-xCORE-AVX512:
Objetivo: indicar explícitamente al compilador que genere código que utilice el conjunto de instrucciones Intel Advanced Vector Extensions 512 (AVX-512).
Características clave: AVX-512 es un conjunto de instrucciones SIMD (instrucción única, datos múltiples) avanzado que ofrece registros vectoriales más amplios y operaciones adicionales en comparación con versiones anteriores como AVX2. Habilitar este indicador permite al compilador aprovechar estas funciones avanzadas para optimizar el rendimiento.
Consideraciones: la portabilidad vuelve a ser la culpable aquí. Es posible que los archivos binarios generados con instrucciones AVX-512 no se ejecuten de manera óptima en procesadores que no admitan este conjunto de instrucciones. ¡Puede que no funcionen en absoluto!
De forma predeterminada, “ -xCORE-AVX512 ” supone que es poco probable que el programa se beneficie del uso de registros zmm. El compilador evita el uso de registros zmm a menos que se garantice una ganancia de rendimiento.
Si se planea utilizar los registros zmm sin restricciones, " " se puede configurar en alto. Eso es lo que haremos también.
¡Guau!
Lo notable es que logramos estos resultados sin ninguna intervención manual sustancial, simplemente incorporando un puñado de indicadores del compilador durante el proceso de compilación de la aplicación.
Nota: No se preocupe si su hardware no es compatible con AVX-512. El compilador Intel C++ admite optimizaciones para AVX, AVX-2 e incluso SSE. ¡La tiene todo lo que necesitas saber!
IPO es un proceso de varios pasos que se centra en las interacciones entre diferentes funciones o procedimientos dentro de un programa. La IPO puede incluir muchos tipos diferentes de optimizaciones, incluida la sustitución directa, la conversión de llamadas indirectas y la inserción.
-ipó:
Objetivo: permite la optimización entre procedimientos, lo que permite al compilador analizar y optimizar todo el programa, más allá de los archivos fuente individuales, durante la compilación.
Características clave: - Optimización de todo el programa: " -ipo " realiza análisis y optimización en todos los archivos fuente, considerando las interacciones entre funciones y procedimientos en todo el programa. - Optimización entre funciones y módulos: la bandera facilita la inserción de funciones y la sincronización de optimizaciones y análisis de flujo de datos en diferentes partes del programa.
Consideraciones: Requiere un paso de enlace independiente. Después de compilar con " -ipo ", se necesita un paso de enlace particular para generar el ejecutable final. El compilador realiza optimizaciones adicionales basadas en la vista completa del programa durante la vinculación.
-IP:
Objetivo: permite la propagación y el análisis entre procedimientos, lo que permite al compilador realizar algunas optimizaciones entre procedimientos sin requerir un paso de enlace por separado.
Características clave: - Análisis y propagación: " -ip " permite al compilador realizar investigaciones y propagación de datos a través de diferentes funciones y módulos durante la compilación. Sin embargo, no realiza todas las optimizaciones que requieren la vista completa del programa. - Compilación más rápida: a diferencia de " -ipo ", " -ip " no necesita un paso de enlace separado, lo que resulta en tiempos de compilación más rápidos. Esto puede resultar beneficioso durante el desarrollo cuando la retroalimentación rápida es esencial.
Consideraciones: solo se producen algunas optimizaciones interprocedimientos limitadas, incluida la incorporación de funciones.
-ipo generalmente proporciona capacidades de optimización interprocedimientos más amplias, ya que implica un paso de enlace separado, pero tiene el costo de tiempos de compilación más largos. [ ] -ip es una alternativa más rápida que realiza algunas optimizaciones entre procedimientos sin requerir un paso de enlace separado, lo que la hace adecuada para las fases de desarrollo y prueba.[ ]
Dado que solo estamos hablando de rendimiento y diferentes optimizaciones, los tiempos de compilación o el tamaño del ejecutable no son de nuestra incumbencia, nos centraremos en " -ipo ".
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
La función jacobi() toma un par de punteros para duplicarlos como parámetros y luego hace algo dentro de los bucles for anidados. Cuando cualquier compilador ve esta función en el archivo fuente, debe tener mucho cuidado.
La expresión para calcular unew usando u implica el promedio de 4 valores de u vecinos. ¿Qué pasa si tanto u como unnew apuntan al mismo lugar? Este se convertiría en el clásico problema de los punteros con alias [ ].
Los compiladores modernos son muy inteligentes y, para garantizar la seguridad, suponen que el alias podría ser posible. Y para escenarios como este, evitan cualquier optimización que pueda afectar la semántica y la salida del código.
En nuestro caso, sabemos que u y unew son ubicaciones de memoria diferentes y están destinadas a almacenar valores diferentes. Por lo tanto, podemos informarle fácilmente al compilador que no habrá ningún alias aquí.
Hay dos métodos. La primera es la palabra clave C “ ” . Pero requiere cambiar el código. No queremos eso por ahora.
¿Algo sencillo? Probemos con " -fno-alias ".
-fno-alias:
Objetivo: indicar al compilador que no asuma alias en el programa.
Características clave: Suponiendo que no haya alias, el compilador puede optimizar más libremente el código, mejorando potencialmente el rendimiento.
Consideraciones: el desarrollador debe tener cuidado al utilizar este indicador, ya que en caso de cualquier alias injustificado, el programa puede generar resultados inesperados.
Pues ya tenemos algo!!!
Un examen más detallado del código ensamblador (aunque no se comparte aquí) y el informe de optimización de compilación generado (ver más abajo ) revela la inteligente aplicación del compilador de y . Estas transformaciones contribuyen a un rendimiento altamente optimizado, lo que muestra el impacto significativo de las directivas del compilador en la eficiencia del código.
El compilador Intel C++ proporciona una característica valiosa que permite a los usuarios generar un informe de optimización que resume todos los ajustes realizados con fines de optimización [ ]. Este informe completo se guarda en formato de archivo YAML y presenta una lista detallada de las optimizaciones aplicadas por el compilador dentro del código. Para obtener una descripción detallada, consulte la documentación oficial en " ".
De manera similar, los compiladores Intel C++ (y todos los populares) también admiten directivas pragma, que son características muy interesantes. Vale la pena consultar algunos de los pragmas como ivdep, paralelo, simd, vector, etc., en .