Anda di halaman 1dari 25

Memoria práctica de

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:

Se ha escogido para esta práctica de optimización la transformada seno discreta de un vector


de datos (en adelante DST). La fórmula general de esta transformada es:

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:

Se ha adaptado el código proporcionado para las prácticas de laboratorio con el programa


GoingFaster para usarlo como herramienta de pruebas de las distintas versiones del algoritmo.
Las pruebas se han hecho sobre Raspberry Pi modelo 2 en el laboratorio y modelo 3 en casa del
estudiante. Por simplicidad se mostrarán los resultados obtenidos para la Raspberry pi 3, aunque
son extrapolables a el modelo 2, ya que ambas funcionaban bajo Raspbian de 32 bits, lo que
básicamente dejaba las diferencias entre los dos modelos en un 33% de aumento de la velocidad
de reloj (900MHz vs 1200 MHz)

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

Se ha considerado el valor de GFlops/seg el mejor indicador de los recogidos porque es en


general independiente del tamaño del vector de entrada y nos permite comparar fácilmente
distintas versiones del algoritmo.

 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

 Algoritmo sin optimizar:

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:

Size: 64 Gflop/s: 0.0264


Size: 128 Gflop/s: 0.0279
Size: 256 Gflop/s: 0.0293
Size: 512 Gflop/s: 0.0283
Size: 2048 Gflop/s: 0.0292
Size: 8192 Gflop/s: 0.0293
Size: 16384 Gflop/s: 0.0293
En este caso, observamos que el mayor número de GFlops / seg se da con un vector de tamaño
256, produciéndose una bajada apreciable del rendimiento con el vector de 512 elementos, que
se va recuperando paulatinamente hasta quedar en la cifra de 0,0293. Teóricamente podríamos
usar el algoritmo para tamaños mayores del vector, ya que vamos calculando los valores sobre
la marcha, pero la escasa velocidad conseguida hace que los cálculos tarden demasiado para
aplicaciones prácticas.

 Algoritmo con optimización del compilador -o1, -o2,-o3:

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:

Obteniendo una mejora de los resultados como era de esperar

Compilado con –o0

Size: 64 Gflop/s: 0.0313


Size: 128 Gflop/s: 0.0311
Size: 256 Gflop/s: 0.0311
Size: 512 Gflop/s: 0.031
Size: 2048 Gflop/s: 0.031
Size: 8192 Gflop/s: 0.031

Compilado con -03

Size: 64 Gflop/s: 0.0354


Size: 128 Gflop/s: 0.0352
Size: 256 Gflop/s: 0.0351
Size: 512 Gflop/s: 0.035
Size: 2048 Gflop/s: 0.035
Size: 8192 Gflop/s: 0.035

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.

Fallos de predicción de salto para el algoritmo inicial

Se han hecho varias pruebas desenrollando el bucle interno, obteniéndose los mejores
resultados con un factor de desenrollado de 16.

Sin optimización del compilador obtenemos estos resultados:

Size: 64 Gflop/s: 0.032


Size: 128 Gflop/s: 0.0318
Size: 256 Gflop/s: 0.0317
Size: 512 Gflop/s: 0.0317
Size: 2048 Gflop/s: 0.0316
Size: 8192 Gflop/s: 0.0316

Con la optimización –o3 obtenemos:

Size: 64 Gflop/s: 0.0371


Size: 128 Gflop/s: 0.0365
Size: 256 Gflop/s: 0.0362
Size: 512 Gflop/s: 0.0362
Size: 2048 Gflop/s: 0.0361
Size: 8192 Gflop/s: 0.0361

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

Sin optimización del compilador obtenemos:

Size: 64 Gflop/s: 0.0324


Size: 128 Gflop/s: 0.032
Size: 256 Gflop/s: 0.0317
Size: 512 Gflop/s: 0.0316
Size: 2048 Gflop/s: 0.0316
Size: 8192 Gflop/s: 0.0316

Lo que es prácticamente igual al resultado obtenido con el desenrollado sólo del bucle interno

Con optimización -o3 obtenemos:

Size: 64 Gflop/s: 0.0362


Size: 128 Gflop/s: 0.0359
Size: 256 Gflop/s: 0.0358
Size: 512 Gflop/s: 0.0357
Size: 2048 Gflop/s: 0.0357
Size: 8192 Gflop/s: 0.0356

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.

El algoritmo pasará ahora a tomar el vector de entrada y multiplicarlo y acumular elemento a


elemento por los elementos de la fila i de la matriz de senos para obtener el elemento i del
vector de salida.

La implementación de la matriz de senos se ha hecho utilizando un vector de longitud N x N, en


el que las posiciones de 0 a N-1 corresponden a la primera fila, de N a 2N-1, corresponden a la
segunda fila y en general las posiciones de (i-1) x N a (i x N) – 1 corresponden a la fila i de la
matriz.

Página 8 de 24
Tras ejecutar las pruebas obtenemos los siguientes resultados:

Para la versión con la optimización del compilador desactivada

Size: 32 Gflop/s: 0.0489


Size: 64 Gflop/s: 0.049
Size: 128 Gflop/s: 0.0488
Size: 256 Gflop/s: 0.0487
Size: 512 Gflop/s: 0.0484
Size: 2048 Gflop/s: 0.0469
Size: 8192 Gflop/s: 0.0455

Para la versión con la optimización del compilador -o3

Size: 32 Gflop/s: 0.0682


Size: 64 Gflop/s: 0.0681
Size: 128 Gflop/s: 0.0677
Size: 256 Gflop/s: 0.0676
Size: 512 Gflop/s: 0.067
Size: 2048 Gflop/s: 0.0646
Size: 8192 Gflop/s: 0.0617

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

 Desenrollado del bucle de creación de la matriz de senos:

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:

Para la versión con la optimización del compilador desactivada

Size: 32 Gflop/s: 0.0463


Size: 64 Gflop/s: 0.0478
Size: 128 Gflop/s: 0.0484
Size: 256 Gflop/s: 0.0489
Size: 512 Gflop/s: 0.0489
Size: 2048 Gflop/s: 0.048
Size: 8192 Gflop/s: 0.0471

Página 10 de 24
Para la versión con la optimización del compilador -o3

Size: 32 Gflop/s: 0.0571


Size: 64 Gflop/s: 0.0645
Size: 128 Gflop/s: 0.0656
Size: 256 Gflop/s: 0.0663
Size: 512 Gflop/s: 0.0663
Size: 2048 Gflop/s: 0.0648
Size: 8192 Gflop/s: 0.0629

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.

 Desenrollado de los bucles para el cálculo de la matriz de senos y de la transformada:

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.

Para la versión sin optimización del compilador:

Size: 32 Gflop/s: 0.0476


Size: 64 Gflop/s: 0.0492
Size: 128 Gflop/s: 0.0499
Size: 256 Gflop/s: 0.0503
Size: 512 Gflop/s: 0.0505
Size: 2048 Gflop/s: 0.0502
Size: 8192 Gflop/s: 0.0492

Para la versión con optimización –o3

Size: 32 Gflop/s: 0.0631


Size: 64 Gflop/s: 0.0655
Size: 128 Gflop/s: 0.0665
Size: 256 Gflop/s: 0.0673
Size: 512 Gflop/s: 0.0673
Size: 2048 Gflop/s: 0.067
Size: 8192 Gflop/s: 0.0649

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

 Uso de NEON en el cálculo de DST:

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.

Obtenemos los siguientes resultados:

Size: 32 Gflop/s: 0.0724


Size: 64 Gflop/s: 0.0725
Size: 128 Gflop/s: 0.072
Size: 256 Gflop/s: 0.0717
Size: 512 Gflop/s: 0.0705
Size: 2048 Gflop/s: 0.0682
Size: 8192 Gflop/s: 0.0666

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:

Probamos ahora a desenrollar el bucle en el que se calcula la transformada, usando 4


operaciones NEON de multiplicación y acumulación, para conseguir un desenrollado equivalente
a 16 en el bucle interno del cálculo del vector salida obteniendo los siguientes resultados:

Size: 32 Gflop/s: 0.0722


Size: 64 Gflop/s: 0.0727
Size: 128 Gflop/s: 0.0728
Size: 256 Gflop/s: 0.0725
Size: 512 Gflop/s: 0.0716
Size: 2048 Gflop/s: 0.0692
Size: 8192 Gflop/s: 0.0662

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.

 Uso de NEON para calcular los senos:

Usando la librería neon_mathfun.h, que hemos descargado de la página señalada en el párrafo


anterior, creamos una versión del algoritmo que calcula la matriz de senos usando la función
sin_ps(), a la que proporcionamos un vector de 4 ángulos de tipo float32x4_t, y que nos devuelve
4 senos en un vector también de tipo float32x4_t. Una vez rellenada la matriz de senos, usamos
vmlaq_f32 para calcular el vector de salida multiplicando 4 elementos del vector de entrada por
las posiciones de la matriz de senos a la vez. Es de esperar que esta mejora aumente el
desempeño del algoritmo de forma significativa, ya que estamos reduciendo el número de
instrucciones necesarias para calcular los senos, que es la parte más costosa
computacionalmente del algoritmo y además lo estamos haciendo con entradas de tipo float en
lugar de double.

Página 14 de 24
Código del algoritmo usando NEON para el cálculo de la matriz de senos

Los resultados obtenidos son:

Size: 32 Gflop/s: 0.418


Size: 64 Gflop/s: 0.427
Size: 128 Gflop/s: 0.43
Size: 256 Gflop/s: 0.432
Size: 512 Gflop/s: 0.416
Size: 2048 Gflop/s: 0.427
Size: 8192 Gflop/s: 0.417

Vemos que los resultados son bastante espectaculares en comparación a lo que habíamos visto
hasta ahora.

Encontramos la dificultad de que la comprobación de resultados inicialmente nos informaba de


errores con tamaños del vector de entrada a partir de 8192, pero de una magnitud alrededor de
10-5. Esto es debido a la menor precisión de los cálculos con datos de tipo float y a la acumulación
de estos errores de precisión cuando operamos en vectores de gran tamaño. Este tipo de
discrepancia con los valores ofrecidos por la versión sin optimizar del algoritmo se considera
admisible y se ha modificado la comprobación de los resultados para obviar esta diferencia no
significativa.

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:

Se ha implementado el algoritmo usando la multiplicación vectorial de NEON para el producto


de los ángulos por la constante factor y también para la multiplicación de la constante raíz por
los elementos del vector salida o sin usarla, como se muestra en la figura anterior, obteniendo
resultados similares en ambos casos, por lo que se ha optado por esta versión simplemente
porque el código es más fácil de leer. Los resultados obtenidos son:

Size: 32 Gflop/s: 0.49


Size: 64 Gflop/s: 0.455
Size: 128 Gflop/s: 0.458
Size: 256 Gflop/s: 0.46
Size: 512 Gflop/s: 0.46
Size: 2048 Gflop/s: 0.461
Size: 8192 Gflop/s: 0.46
Size: 16364 Gflop/s: 0.46
Size: 32768 Gflop/s: 0.461
Size: 65536 Gflop/s: 0.461

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.

Fallos de caché y de predicción de rama en el algoritmo con matriz de senos

Fallos de caché y de predicción de rama en el algoritmo sin matriz de senos

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.

Ciclos de procesador por símbolo en la versión sin desenrollado

Página 17 de 24
Ciclos de procesador por símbolo en la versión con desenrollado

Uso del paralelismo a nivel de hilo

Aunque el paralelismo a nivel de thread no es propiamente una forma de paralelismo a nivel de


instrucción, la presencia de 4 núcleos en el procesador nos deja abierta la posibilidad de usarlos
de forma concurrente para dividir el trabajo entre los núcleos disponibles, lo cual es tentador
para conseguir un mayor rendimiento del hardware disponible.

 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:

Size: 32 Gflop/s: 1.24


Size: 64 Gflop/s: 1.63
Size: 128 Gflop/s: 1.81
Size: 256 Gflop/s: 1.85
Size: 512 Gflop/s: 1.86
Size: 2048 Gflop/s: 1.87
Size: 8192 Gflop/s: 1.87
Size: 16364 Gflop/s: 1.86
Size: 32768 Gflop/s: 1.87
Size: 65536 Gflop/s: 1.86

Evidentemente, dividir el trabajo en sólo 2 hilos desaprovecharía 2 núcleos del procesador. La


división del trabajo en un número mayor de 4 de hilos hace que el desempeño caiga para
tamaños de vector de entrada pequeños debido al mayor overhead provocado por la creación y
gestión de las tareas para los hilos , pero para tamaños grandes se iguala el rendimiento y éste
es comparable a la versión de 4 hilos.

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

 Optimizaciones con operaciones unarias sobre los datos:

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:

+ El cálculo adelantado de las constantes factor y raíz y la subsecuente sustitución de las


divisiones por productos en los lugares donde aparecían, lo que aumentó el rendimiento
del algoritmo alrededor del 21%.
+ El desenrollado del bucle interno del bucle de cálculo de la DST, que consiguió una
mejora de alrededor del 9% adicional.

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.

En cuanto a la parte negativa de estas optimizaciones tenemos:

- Se hace notar que el método de compartimentación o tiling no fue efectivo para


aumentar el rendimiento debido a que sólo se reutilizan los datos del vector de entrada
y el acceso a todos los datos se hace de forma secuencial siempre.
- El uso de una matriz de senos, aunque ha aumentado el rendimiento, tiene las
desventajas de un uso ávido de memoria, que limita efectivamente el tamaño máximo
del vector de entrada que podemos aceptar y también que el acceso a la matriz no se
hace de forma secuencial durante el proceso de llenado de la misma, lo que perjudica
el rendimiento del algoritmo progresivamente a medida que va aumentando el tamaño
de esta matriz.

 Optimizaciones con operaciones vectoriales sobre los datos:

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

 Optimización con el uso de paralelismo a nivel de thread:

La última optimización que implementamos aprovecha la existencia de 4 núcleos en el


procesador para repartir la carga entre ellos, de forma que cada núcleo calcule N / 4 elementos
del vector de salida, lo que debería darnos un aumento del rendimiento cercano a 4 x; de hecho,
y con 4 hilos de ejecución, el aumento del rendimiento es de una media del 394%, pero incluso
mayor al 400% en tamaños de vector grandes. Un número mayor de hilos no representa un
aumento del rendimiento en nuestro algoritmo.

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

32u 64u 128u 256u 512u 2048u 8192u

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

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