Considere el siguiente programa de computadora muy simple:
for i = 1 to n:
y[i] = x[p[i]]
Aquí e y son matrices de n elementos de bytes, y p es una matriz de palabras de n elementos. Aquí n es grande, por ejemplo, n = 2 31 (de modo que solo una fracción insignificante de los datos cabe en cualquier tipo de memoria caché).
Suponga que consiste en números aleatorios , distribuidos uniformemente entre 1 y n .
Desde la perspectiva del hardware moderno, esto debería significar lo siguiente:
- leer es barato (lectura secuencial)
- leer es muy costoso (lecturas aleatorias; casi todas las lecturas son errores de caché; tendremos que buscar cada byte individual de la memoria principal)
- escribir es barato (escritura secuencial).
Y esto es de hecho lo que estoy observando. El programa es muy lento en comparación con un programa que solo realiza lecturas y escrituras secuenciales. Excelente.
Ahora viene la pregunta: ¿qué tan bien se paraleliza este programa en las plataformas modernas de múltiples núcleos?
Mi hipótesis era que este programa no se paraleliza bien. Después de todo, el cuello de botella es la memoria principal. Un solo núcleo ya está perdiendo la mayor parte de su tiempo solo esperando algunos datos de la memoria principal.
Sin embargo, esto no fue lo que observé cuando comencé a experimentar con algunos algoritmos donde el cuello de botella era este tipo de operación.
Simplemente reemplacé el bucle for ingenuo con un bucle for paralelo paralelo OpenMP (en esencia, dividirá el rango en partes más pequeñas y ejecutará estas partes en diferentes núcleos de CPU en paralelo).
En las computadoras de gama baja, las aceleraciones fueron menores. Pero en las plataformas de gama alta me sorprendió que estaba obteniendo excelentes aceleraciones casi lineales. Algunos ejemplos concretos (los tiempos exactos pueden estar un poco apagados, hay muchas variaciones aleatorias; estos fueron solo experimentos rápidos):
2 x Xeon de 4 núcleos (en total 8 núcleos): factoriza 5-8 aceleraciones en comparación con la versión de un solo hilo.
2 x Xeon de 6 núcleos (en total 12 núcleos): factoriza entre 8 y 14 aceleraciones en comparación con la versión de un solo hilo.
Ahora esto fue totalmente inesperado. Preguntas:
Precisamente, ¿por qué este tipo de programa es tan paralelo ? ¿Qué pasa en el hardware? (Mi conjetura actual es algo así: las lecturas aleatorias de diferentes hilos están "canalizadas" y la tasa promedio de obtener respuestas a estas es mucho mayor que en el caso de un solo hilo).
¿Es necesario usar múltiples hilos y múltiples núcleos para obtener aceleraciones? Si realmente se lleva a cabo algún tipo de interconexión en la interfaz entre la memoria principal y la CPU, una aplicación de un solo subproceso no podría hacerle saber a la memoria principal que pronto necesitará , x [ p [ i + 1 ] ] , ... ¿y la computadora podría comenzar a buscar las líneas de caché relevantes de la memoria principal? Si esto es posible en principio, ¿cómo lo logro en la práctica?
¿Cuál es el modelo teórico correcto que podríamos usar para analizar este tipo de programas (y hacer predicciones correctas del rendimiento)?
Editar: ahora hay algunos códigos fuente y resultados de referencia disponibles aquí: https://github.com/suomela/parallel-random-read
Algunos ejemplos de figuras de estadio ( ):
- aprox. 42 ns por iteración (lectura aleatoria) con un solo hilo
- aprox. 5 ns por iteración (lectura aleatoria) con 12 núcleos.
fuente
Decidí probar __builtin_prefetch () yo mismo. Lo publico aquí como respuesta en caso de que otros quieran probarlo en sus máquinas. Los resultados están cerca de lo que describe Jukka: aproximadamente una disminución del 20% en el tiempo de ejecución cuando se recuperan 20 elementos por delante frente a 0 elementos por delante.
Resultados:
Código:
fuente
El acceso a DDR3 está canalizado. http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf diapositivas 20 y 24 de muestran lo que sucede en el bus de memoria durante las operaciones de lectura canalizadas.
(parcialmente incorrecto, ver más abajo) No son necesarios varios subprocesos si la arquitectura de la CPU admite la captación previa de caché. Modern x86 y ARM, así como muchas otras arquitecturas, tienen una instrucción de captación previa explícita. Además, muchos intentan detectar patrones en los accesos a la memoria y realizan la captación previa automáticamente. El soporte de software es específico del compilador, por ejemplo, GCC y Clang tienen __builtin_prefech () intrínseco para la captación previa explícita.
El hyperthreading de estilo Intel parece funcionar muy bien para programas que pasan la mayor parte del tiempo esperando errores de caché. En mi experiencia, en la carga de trabajo intensiva de computación, la aceleración va muy poco por encima del número de núcleos físicos.
EDITAR: Me equivoqué en el punto 2. Parece que si bien la captación previa puede optimizar el acceso a la memoria para un solo núcleo, el ancho de banda de memoria combinado de múltiples núcleos es mayor que el ancho de banda de un solo núcleo. Cuánto mayor, depende de la CPU.
El prefetcher de hardware y otras optimizaciones juntas hacen que la evaluación comparativa sea muy complicada. Es posible construir casos en los que la captación previa explícita tenga un efecto muy visible o inexistente en el rendimiento, siendo este punto de referencia uno de los últimos.
fuente