Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
optimización de código
ANDRÉS MIGUEL GONZÁLEZ GÓMEZ
27341487F
3º A INGENIERÍA DE COMPUTADORES
Contenido
Introducción.............................................................................................................................. 2
Selección de Código: ............................................................................................................. 2
Programa de prueba: ............................................................................................................ 2
Metodología empleada: ........................................................................................................ 3
Datos recogidos: .................................................................................................................... 3
Cálculo de GFlops/seg: .......................................................................................................... 3
Pruebas realizadas .................................................................................................................... 4
Algoritmo sin optimizar: ........................................................................................................ 4
Algoritmo con optimización del compilador -o1, -o2,-o3: .................................................... 4
Algoritmo sin repetición de operaciones: ............................................................................. 5
Desenrollado de bucles: ........................................................................................................ 6
Tiling: ..................................................................................................................................... 8
Matriz de senos: .................................................................................................................... 8
Desenrollado del bucle de creación de la matriz de senos: ................................................ 10
Desenrollado de los bucles paran el cálculo de la matriz de senos y de la transformada: . 11
Uso de SIMD ........................................................................................................................... 12
Uso de NEON en el cálculo de DST: ..................................................................................... 12
Desenrollado del bucle de cálculo de la transformada usando NEON: .............................. 14
Uso de NEON para calcular los senos:................................................................................. 14
Vuelta al cálculo de los valores del seno sobre la marcha: ................................................. 16
Desenrollado del bucle interno en algoritmo con NEON y sin matriz de senos: ................ 17
Uso del paralelismo a nivel de hilo ........................................................................................ 18
Algoritmo multihilo: ............................................................................................................ 18
Conclusiones ........................................................................................................................... 19
Optimizaciones con operaciones unarias sobre los datos: ................................................. 19
Optimizaciones con operaciones vectoriales sobre los datos: ........................................... 20
Optimización con el uso de paralelismo a nivel de thread: ................................................ 21
Gráficas ................................................................................................................................... 22
Posibles mejoras en el algoritmo ........................................................................................... 24
Página 1 de 24
Introducción
Selección de Código:
Una versión no optimizada del código que implementa esta transformada se obtuvo de
https://people.sc.fsu.edu/~jburkardt/cpp_src/sine_transform/sine_transform.html y
fue adaptada para su uso en esta práctica.
Esta versión no optimizada del código se tomará como base de partida para las distintas
versiones del algoritmo que serán el objeto de esta práctica y servirá asimismo para la
comprobación de la corrección de los resultados obtenidos con las mismas.
Programa de prueba:
Página 2 de 24
Metodología empleada:
La metodología que se ha seguido es crear una versión del código con la que se esperan obtener
mejoras en la velocidad de procesamiento de los datos, principalmente aumentando el
paralelismo a nivel de instrucción. Se ha ejecutado las distintas versiones del código mediante
el programa de prueba sobre una serie de vectores de valores aleatorios con longitudes
crecientes y se han recogido los resultados obtenidos. Posteriormente se comprueba si la
mejora esperada se ha obtenido o no y la magnitud de la misma usando los datos recogidos y
herramientas como Perf y se intenta encajar los resultados obtenidos en los contenidos teóricos
de la asignatura vistos en clase.
Datos recogidos:
Los datos recogidos en para la comparación del desempeño de las distintas versiones de la
función optimizada han sido:
GFlops/seg obtenidos
Iteraciones del vector completo realizadas en el tiempo de prueba
Tiempo por cada iteración
Tiempo total empleado en la prueba
Tiempo por cada unidad de tamaño del vector de salida
Elementos del vector salida calculados por segundo
Cálculo de GFlops/seg:
La determinación de los GFlops/seg que produce la ejecución de las distintas versiones del
algoritmo en el programa de pruebas se ha hecho calculando el número de operaciones en
punto flotante que contiene el algoritmo DST. Se han obviado operaciones de suma y resta y
sólo se tienen en cuenta operaciones de producto, división, raíces y cálculo de senos.
En total realizaremos las siguientes operaciones transcendentes de punto flotante para nuestro
algoritmo y un vector de tamaño N:
N x N senos
N raíces cuadradas
3 x N x N + N productos
N x N + N divisiones
En total tenemos un número de operaciones de punto flotante de 5·N2+3·N para cada llamada
de nuestro programa de prueba a la función que calcula la transformada seno; este dato se ha
introducido en el programa de pruebas para el cálculo de los datos de salida expuestos
anteriormente.
Página 3 de 24
Pruebas realizadas
Se ejecuta el algoritmo sin optimizar el código y con la optimización del compilador desactivada
(-o0) para tener unos valores iniciales que usar en las comparaciones con los siguientes
resultados:
Tras compilar el código no optimizado con las distintas opciones de optimización del compilador
obtenemos los siguientes resultados:
Vemos que desde la optimización –o1 se produce una importante mejora en el rendimiento,
observando el código ensamblador generado, vemos que en las versiones optimizadas se usan
comandos de vector que mejoran el desempeño del programa y disminuyen su tamaño
Página 4 de 24
Algoritmo sin repetición de operaciones:
Examinando el código del algoritmo sin optimizar, vemos que se calcula en cada iteración del
bucle externo la raíz cuadrada de 2/N+1 y que el ángulo se divide cada vez que se calcula en el
bucle interno por un factor de N+1, con lo que se coloca el cálculo de estos valores fuera de los
bucles y se utilizan los valores previamente calculados dentro de los mismos:
Vemos una mejora de alrededor del 8% conseguida disminuyendo el número de cálculos dentro
del bucle principal del algoritmo y hasta del 21% usando la optimización del compilador.
Página 5 de 24
Desenrollado de bucles:
El bloque básico del algoritmo consta de dos líneas en C, que se traducen en unas decenas de
instrucciones ensamblador en el código compilado; podemos intentar desenrollar los bucles
para ver qué mejora conseguimos al aumentar el tamaño del bloque básico y, por tanto,
disminuir la cantidad de operaciones de control de flujo del programa necesarias.
Se han hecho varias pruebas desenrollando el bucle interno, obteniéndose los mejores
resultados con un factor de desenrollado de 16.
Lo que supone una mejora de alrededor de un 6% sobre la anterior optimización del código y
un incremento del 14% sobre la versión inicial; una vez optimizado el código por el compilador
se obtiene una mejora de alrededor del 30% sobre la versión inicial. Esta mejora se debe al
mayor tamaño del bloque básico, lo que disminuye el número de instrucciones de control de
flujo y riesgos asociados y también al mayor número de instrucciones dentro del bloque básico
de las que dispone el compilador para reordenar y evitar riesgos principalmente de tipo RAW,
dada la naturaleza del código.
Página 6 de 24
Posteriormente se prueba con distintos factores de desenrollado de los bucles externo e interno
a la vez obteniéndose los mejores resultados con el desenrollado de los bucles interno y externo
en un factor de 8
Lo que es prácticamente igual al resultado obtenido con el desenrollado sólo del bucle interno
En este caso, la versión optimizada por el compilador no llega a igualar el desempeño del
algoritmo con sólo el bucle interno desenrollado:
Analizamos con Perf ambas versiones del algoritmo y la única diferencia apreciable encontrada
es el número de fallos en la caché de nivel 2 del algoritmo desenrollado en los dos bucles, que
supera en alrededor de un 3% al algoritmo con desenrollado sólo en el bucle interno, lo que
puede explicar la diferencia de rendimiento obtenida. La causa de este mayor número de fallos
de caché se encuentra en que el desenrollado del bucle externo genera referencias al vector
salida que no siempre son secuenciales, lo que perjudica el óptimo funcionamiento de la
memoria caché.
Página 7 de 24
Tiling:
Hasta ahora hemos ido calculando los resultados “sobre la marcha” accediendo al vector
entrada y calculando los ángulos y senos según eran necesarios. Esta forma de acceso secuencial
a los datos no promete mejoras del resultado utilizando la técnica de Tiling, ya que sólo se
reutilizan los datos del vector de entrada, y siempre se accede a ellos de forma secuencial. Se
hacen algunas pruebas obteniendo los siguientes resultados:
Lo que coincide con los resultados esperados. La pérdida de rendimiento puede atribuirse a los
mayores riesgos de control que supone la compartimentación, al requerir un bucle anidado
adicional. Solo se advierte una marginal mejora del rendimiento con tamaños del vector de
entrada mayores que 2048, debido a un menor número de fallos de caché.
Por ahora, se descarta el uso de la técnica de tiling para la mejora de rendimiento en este
algoritmo.
Matriz de senos:
Analizando el algoritmo original, vemos que los ángulos generados para el cálculo de los senos
son simétricos respecto a la diagonal si los colocamos en una matriz N x N, debido a la propiedad
conmutativa del producto.
Se propone crear una matriz donde se almacenen los senos, ya calculados y que se rellenará
aprovechando esta característica de simetría de los datos. De esta manera nos ahorraríamos el
cálculo de N / 2 ángulos y senos, obteniendo los mismos resultados, con lo que se espera una
mejora del rendimiento. Como contrapartida, necesitaremos una gran cantidad de memoria
para almacenar los senos calculados, lo que limita el tamaño máximo del vector de entrada con
el que podemos trabajar. Para un vector de 8192 elementos de tipo float, necesitaremos
8192 x 8192 x 4 = 256MB de memoria sólo para almacenar los senos.
Página 8 de 24
Tras ejecutar las pruebas obtenemos los siguientes resultados:
Lo que supone una diferencia de más del 60% en la versión sin optimizar y de más del doble en
la optimizada con respecto a la versión inicial del algoritmo.
Página 9 de 24
Aún con la gran ganancia de rendimiento, observamos que esta versión del algoritmo pierde
eficiencia gradualmente a medida que aumentamos el tamaño del vector de entrada,
investigando un poco este comportamiento con Perf, descubrimos que es debido al crecimiento
del número de fallos de página asociado al tamaño de la matriz de senos, que como sabemos,
crece en proporción cuadrática con el tamaño del vector.
Fallos de página y localización de los mismos para tamaños del vector de entrada de hasta 512 elementos
Fallos de página y localización de los mismos para tamaños del vector de entrada de 2048 y 8192
Hacemos algunas pruebas con el desenrollado del bucle de creación de la matriz de senos, con
los siguientes resultados para el desenrollado de los bucles interno y externo en un factor de 4:
Página 10 de 24
Para la versión con la optimización del compilador -o3
Vemos que la versión con desenrollado en el bucle de creación de la matriz de senos funciona
peor para tamaños del vector de entrada hasta 512 y algo mejor con los tamaños mayores.
Como es de esperar que los tamaños de entrada de los vectores de datos sean grandes, se
considera positiva esta optimización.
Como última optimización, desenrollamos tanto el bucle de creación de la matriz de senos como
el que calcula el vector de salida con los parámetros que han funcionado mejor en nuestras
pruebas.
Página 11 de 24
Vemos que la diferencia con la anterior optimización es de casi el 6% en la versión sin optimizar,
y prácticamente despreciable en la versión optimizada. Tras investigar un poco con Perf,
llegamos a la conclusión de que el desenrollado de la matriz de senos más el cálculo de DST,
genera un código optimizado más largo, alrededor del 1%, lo que penaliza el rendimiento del
algoritmo.
Uso de SIMD
En este punto, poco más se puede hacer en cuanto a la optimización del algoritmo DST mediante
el aumento del paralelismo a nivel de instrucción con instrucciones que sólo operan sobre un
dato a la vez; lo que sí parece factible es utilizar las extensiones NEON de ARM para intentar
paralelizar las operaciones de cálculo de nuestro algoritmo, de forma que se aumente
significativamente su rendimiento. Para todas las pruebas con las extensiones NEON hemos
usado los siguientes parámetros del compilador:
-O3 -mcpu=cortex-a7 -mtune=cortex-a7 -mfloat-abi=hard -mfpu=NEON-vfpv4 -funsafe-math-
optimizations
En una primera prueba del uso de NEON, usamos la instrucción vmlaq_f32 para multiplicar y
acumular 4 elementos del vector de entrada por las posiciones de la matriz de senos a la vez.
Página 12 de 24
Es de esperar una mejora significativa, aunque no que multiplique por 4 el desempeño del
programa, ya que la operación computacionalmente más costosa es el cálculo de los senos, que
seguimos haciendo de uno en uno.
Lo que supone una mejora del 30% sobre la versión del código que no usa NEON equivalente.
Sin embargo, notamos que el rendimiento del algoritmo baja rápidamente con el tamaño del
vector de entrada; tras analizar el algoritmo con Perf, llegamos a la conclusión de que en los
tamaños de vector más grandes aumentan en gran medida los fallos de la caché de segundo
nivel y los fallos de página, debido al tamaño de la matriz de senos y al acceso no secuencial a la
misma.
Fallos de página y localización de los mismos para tamaños del vector de entrada de hasta 512 elementos
Fallos de página y localización de los mismos para tamaños del vector de entrada de 2048 y 8192
Página 13 de 24
Desenrollado del bucle de cálculo de la transformada usando NEON:
Observamos una pequeña mejora en los resultados, pero seguimos teniendo el problema de la
caída de rendimiento para tamaños grandes del vector de entrada.
Llegados a este punto, vemos que estamos limitados por el uso de la matriz de senos en dos
sentidos:
Por un lado, tiene unos requerimientos de memoria que nos limitan el tamaño máximo
del vector de entrada que podemos aceptar
Por otro lado, el cálculo de los senos resulta ser la parte más costosa
computacionalmente y seguimos realizándolo de uno en uno.
Un poco de investigación por internet sobre NEON y el cálculo de senos, nos lleva a encontrar el
sitio http://gruntthepeon.free.fr/ssemath/NEON_mathfun.html donde podemos encontrar una
librería que nos permite usar NEON para calcular 4 senos de tipo float de forma simultánea
usando NEON, lo que debería traer una mejora de desempeño significativa a nuestro algoritmo.
Página 14 de 24
Código del algoritmo usando NEON para el cálculo de la matriz de senos
Vemos que los resultados son bastante espectaculares en comparación a lo que habíamos visto
hasta ahora.
Página 15 de 24
Vuelta al cálculo de los valores del seno sobre la marcha:
Como nos encontramos que el desempeño de nuestro algoritmo se ve afectado por el tamaño
de la matriz de senos, se prueba a hacer una versión que vuelva a calcular los senos sobre la
marcha, sin almacenarlos en la matriz de senos, pero aprovechando las instrucciones NEON para
el cálculo de senos y la salida:
Vemos como se estabiliza el rendimiento del algoritmo y se mantiene con tamaños del vector
de entrada superiores. Inesperadamente, vemos que esta versión del algoritmo tiene un
Página 16 de 24
rendimiento mayor que la versión con matriz de senos, que sólo calcula la mitad de los mismos.
Esto se puede explicar porque en este caso el acceso a los datos es siempre secuencial en el
vector de entrada y los senos se calculan sobre la marcha, con lo que se evitan fallos de caché y
de página y también por la ausencia de estructuras de control en la generación de la matriz de
senos, que crean riesgos de control.
Analizando ambos algoritmos con Perf, vemos como los fallos de caché son un orden de
magnitud mayores en el algoritmo con matriz de senos; asimismo los fallos de predicción de
rama son alrededor de un 45% superiores.
Desenrollado del bucle interno en algoritmo con NEON y sin matriz de senos:
Probamos varios factores de desenrollado en el bucle interno del algoritmo, pero no obtenemos
resultados positivos. El análisis del algoritmo con Perf, nos muestra que en la versión
desenrollada del código, se pierde gran número de ciclos en el cálculo de los senos, lo que nos
indica que se han producido stalls en las instrucciones de cálculo de los senos. Una posible
explicación a esto se da en las conclusiones de este documento.
Página 17 de 24
Ciclos de procesador por símbolo en la versión con desenrollado
Algoritmo multihilo:
Se adapta el algoritmo más eficiente hasta ahora, que usa las extensiones NEON en el cálculo de
los senos y la transformada y se divide el cálculo del vector de salida en bloques de tamaño
(N / nº threads) que son asignados a hilos que se ejecutarán de forma concurrente.
Página 18 de 24
Se hacen pruebas con un número de hilos desde 2, 4, 8, 16 y 32, obteniéndose los mejores
resultados con 4 hilos; se esperaba que ésta fuera la opción que produce mejores resultados ya
que nuestro algoritmo original estaba pensado para ejecutarse en un solo núcleo. Los resultados
obtenidos son:
Para tamaños de la entrada grandes, se consigue un rendimiento un poco por encima del 400%
respecto a la versión monothread del algoritmo, debido a que la división del trabajo en hilos crea
una suerte de compartimentación.
Conclusiones
De los resultados obtenidos en las diversas pruebas realizadas sobre las versiones del algoritmo
DST que se han creado para esta práctica podemos concluir que, para este algoritmo en
particular y sin el uso de las operaciones vectoriales NEON, las técnicas de desenrollado y
compartimentación generan un incremento del rendimiento que, aunque perceptible, no es
muy significativo habida cuenta de los resultados finales obtenidos.
Las optimizaciones que más han afectado al rendimiento del algoritmo antes de usar NEON han
sido:
Página 19 de 24
+ La creación de una matriz de senos, en la que aprovechábamos la característica de
simetría respecto a la diagonal principal de los valores de los senos, lo que
efectivamente nos ahorraba el cálculo de la mitad de los senos y que aumentó el
rendimiento un 92% aproximadamente.
+ El desenrollado de los bucles interno y externo de creación de la matriz de senos en un
factor de 4, que nos supuso alrededor de un 14% adicional
En total, estas mejoras han conseguido aumentar el rendimiento del algoritmo alrededor
del 136%, lo que no está mal, pero es insuficiente para tener un rendimiento aceptable con
vectores de longitud elevada.
Una vez que hemos llegado al límite de lo obtenible con operaciones unarias sobre los datos,
pasamos a aumentar el paralelismo a nivel de instrucción utilizando operaciones vectoriales
sobre los datos, aprovechando las extensiones SIMD NEON del juego de instrucciones del
procesador.
Las optimizaciones que más han afectado al rendimiento del algoritmo usando NEON han sido:
+ Uso de la instrucción vmlaq_f32 para multiplicar y acumular los datos de los vectores de
entrada y la fila correspondiente de la matriz de senos de 4 en 4. Esto aumentó el
rendimiento en alrededor de un 30% adicional.
+ Desenrollado del bucle interno del cálculo de DST en un factor de 4, lo que, junto al uso
de NEON, consigue hacer 16 cálculos en cada iteración del bucle. El aumento de
rendimiento conseguido es de aproximadamente un 2% adicional.
+ Uso de instrucciones vectoriales en el cálculo de los senos. El uso de la función sin_ps de
la librería neon_mathfun.h nos permite calcular 4 senos a la vez aprovechando las
extensiones NEON, lo que, al ser el cálculo de senos la parte más pesada
computacionalmente del algoritmo y reducirse la precisión de los cálculos al usar datos
de tipo float en lugar de double, nos permite aumentar el rendimiento en un factor
cercano a 6 respecto a la mejor optimización sin usar instrucciones SIMD. (!)
+ Tras abandonar el uso de la matriz de senos y pasar a obtener los senos en el momento
que son necesarios para los cálculos, vemos que el rendimiento ha aumentado un 8.5%
adicional hasta un factor aproximado de 7, además haciendo el rendimiento en
GFlops/seg del algoritmo independiente del tamaño del vector de entrada y liberando
memoria para poder aceptar entradas de mucho mayor tamaño.
Página 20 de 24
En cuanto a la parte negativa de estas optimizaciones tenemos:
- Como contrapartida al aumento de velocidad en los cálculos, hay una pérdida de
precisión en los mismos al realizar todas las operaciones con datos de tipo float y no
double, especialmente en el cálculo de los senos.
- Errores de acumulación provocados por esta pérdida de precisión y la gran cantidad de
operaciones de suma y acumulación realizadas en vectores de gran tamaño. El efecto
empieza a ser perceptible a partir de vectores de 8K elementos, con errores de precisión
del orden de 10-5 en comparación con los cálculos con valores double, pero estos errores
son acumulativos y serán mayores en vectores más largos.
- El desenrollado de bucles parece no conseguir aumentar el rendimiento cuando se usan
las extensiones NEON; parece que esto tiene que ver con la longitud de las cola de
instrucciones y datos en la unidad NEON del procesador:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344b/BABFFGIG.h
tml
En total, y respecto a la versión inicial sin optimizar del algoritmo, hemos conseguido aumentar
el rendimiento en un factor de 63x y respecto a la versión optimizada sin usar SIMD en un factor
de 27X.
Página 21 de 24
Gráficas
2,5
1,5
0,5
0
Naive-o0 Naive-o3 Unroll j=16 - Unroll i=i=8 - Tiling=512 - Matriz-senos Matriz-senos Unroll senos
o3 o3 o3 -o3 unroll -o3 y DST -o3
Rendimiento de las distintas optimizaciones para distintos tamaños de vector de prueba sin usar SIMD
2,5
1,5
0,5
0
Naive-o0 Naive-o3 Unroll j=16 - Unroll i=i=8 - Tiling=512 - Matriz-senos Matriz-senos Unroll senos
o3 o3 o3 -o3 unroll -o3 y DST -o3
Media de rendimiento de las optimizaciones sin SIMD para los tamaños de vector de prueba testados
Página 22 de 24
70
60
50
40
30
20
10
0
Naive-o0 Matriz senos + NEON NEON en senos+DST Naive con NEON x 2 NEON + Threads x 4
+ unroll
Media de rendimiento de las optimizaciones con SIMD en comparación con la versión sin optimizar
100
10
1
32u
64u
128u
256u
512u
2048u
8192u
Comparativa de todas las optimizaciones para tamaños del vector de entrada probados en escala logarítmica
Página 23 de 24
Posibles mejoras en el algoritmo
Al igual que existe una simetría en los valores de los senos respecto a la diagonal principal en la
matriz de senos, también existen otras simetrías respecto a los ejes vertical y horizontal de la
misma en cuanto al valor absoluto de los valores, aunque el signo de los mismos no mantiene
esta simetría. Asimismo y debido a la naturaleza cíclica de la función seno, muchos de los valores
se repiten, o son iguales y de signo contrario. Sería necesario un estudio más detallado de estos
factores para conseguir disminuir el número de veces que calculamos los senos, de forma que
se aumente el rendimiento al no ser necesario calcular senos de ángulos separados por un
múltiplo de 2π o complementarios por ser iguales, o suplementarios por ser iguales y de signo
distinto. Posiblemente este tipo de mejoras lleven a implementar una transformada seno rápida,
algo que queda fuera del alcance de esta práctica.
Página 24 de 24