rendering - que - Hyper-threading... hizo mi renderizador 10 veces más lento
hyper threading que es (3)
Básicamente, necesita una forma bastante portátil de consultar el entorno para obtener detalles de hardware de un nivel bastante bajo, y en general, no puede hacerlo solo con llamadas al sistema (el sistema operativo generalmente desconoce incluso la diferencia entre los hilos y núcleos de hardware).
Una biblioteca que admite varias plataformas es hwloc : admite Linux y Windows (y otras), chips Intel y AMD. Hwloc le permitirá descubrir todo sobre la topología del hardware y conoce la diferencia entre los núcleos y los hilos de hardware (denominados PU, unidades de procesamiento, en la terminología de hwloc). Entonces llamaría a esta biblioteca al comienzo, buscaría la cantidad de núcleos reales y llamaría a omp_set_num_threads () (o simplemente agregaría esa variable como una directiva al comienzo de las secciones paralelas).
Resumen ejecutivo : ¿Cómo se puede especificar en su código que OpenMP solo debe usar subprocesos para los núcleos REAL, es decir, no contar los de subproceso?
Análisis detallado : A lo largo de los años, he codificado un renderizador de código abierto (rasterizador / rayo de rayos) solo para SW en mi tiempo libre. El código GPL y los binarios de Windows están disponibles desde aquí: https://www.thanassis.space/renderer.html Se compila y funciona bien en Windows, Linux, OS / X y los BSD.
Introduje un modo de trazado de rayos este último mes, y la calidad de las imágenes generadas se disparó. Desafortunadamente, el trazado de rayos es más lento que la rasterización. Para aumentar la velocidad, al igual que lo hice con los rasterizadores, agregué soporte OpenMP (y TBB) al trazador de rayos, para hacer uso fácilmente de núcleos de CPU adicionales. Tanto el rasterizado como el trazado de rayos son fáciles de enhebrar (trabajo por triángulo, trabajo por píxel).
En casa, con mi Core2Duo, el segundo núcleo ayudó a todos los modos: tanto el modo de rasterización como el de trazado de rayos obtuvieron una aceleración de entre 1.85x y 1.9x.
El problema: Naturalmente, tenía curiosidad por ver el rendimiento superior de la CPU (también "juego" con GPU, puerto CUDA preliminar ), así que quería una base sólida para las comparaciones. Le di el código a un buen amigo mío, que tiene acceso a una máquina "bestia", con un super procesador de 16 núcleos y 1500 $ Intel.
Lo ejecuta en el modo "más pesado", el modo de trazador de rayos ...
... y obtiene una quinta parte de la velocidad de mi Core2Duo (!)
Jadeo - horror. ¿Lo que acaba de suceder?
Comenzamos a probar diferentes modificaciones, parches, ... y finalmente lo resolvimos.
Al utilizar la variable de entorno OMP_NUM_THREADS, se puede controlar cuántos subprocesos OpenMP se generan. A medida que el número de hilos aumentaba de 1 a 8, la velocidad aumentaba (cerca de un aumento lineal). En el momento en que cruzamos el 8, la velocidad comenzó a disminuir, hasta que se redujo a una quinta parte de la velocidad de mi Core2Duo, ¡cuando se usaron los 16 núcleos!
¿Por qué 8?
Porque 8 era el número de los núcleos reales . Los otros 8 fueron ... ¡hiper-roscando!
La teoría: ahora, esto fue una novedad para mí: he visto mucha ayuda de subprocesos (hasta un 25%) en otros algoritmos, así que esto fue inesperado. Aparentemente, a pesar de que cada núcleo de subprocesamiento hiperactivo viene con sus propios registros (¿y unidad SSE?), El trazador de rayos no pudo hacer uso de la potencia de procesamiento adicional. Lo que me lleva a pensar ...
Probablemente no sea la potencia de procesamiento lo que está muerto de hambre, es el ancho de banda de la memoria.
El trazador de rayos utiliza una estructura de datos de jerarquía de volumen delimitador, para acelerar las intersecciones entre triángulo y rayo. Si se utilizan los núcleos de subprocesamiento, cada uno de los "núcleos lógicos" en un par, está intentando leer desde diferentes lugares en esa estructura de datos (es decir, en la memoria), y las cachés de CPU (local por par) están completamente destrozadas. Al menos, esa es mi teoría, cualquier sugerencia que sea bienvenida.
Por lo tanto, la pregunta: OpenMP detecta el número de "núcleos" y genera subprocesos para que coincidan, es decir, incluye los "núcleos" en el cálculo. En mi caso, esto aparentemente conduce a resultados desastrosos, en cuanto a velocidad. ¿Alguien sabe cómo usar la API de OpenMP (si es posible, portátil) para generar solo hilos para los núcleos REALES, y no los hipervínculos?
PS El código está abierto (GPL) y está disponible en el enlace anterior. Siéntase libre de reproducirlo en su propia máquina. Supongo que esto sucederá en todas las CPU con hipervínculos.
PPS Disculpe la longitud del post, pensé que era una experiencia educativa y quería compartir.
Desafortunadamente, su suposición sobre por qué ocurre esto es muy probablemente correcta. Para estar seguro, tendría que usar una herramienta de perfil, pero he visto esto antes con el trazado de rayos, por lo que no es sorprendente. En cualquier caso, actualmente no hay una manera de determinar a partir de OpenMP que algunos de los procesadores son "reales" y otros están hipercercados. Podría escribir algún código para determinar esto y luego establecer el número usted mismo. Sin embargo, aún existe el problema de que OpenMP no programa los subprocesos en los procesadores, ya que permite que el sistema operativo lo haga.
Se ha trabajado en el comité de idiomas de OpenMP ARB para tratar de definir una forma estándar para que el usuario determine su entorno y diga cómo ejecutar. En este momento, esta discusión todavía está en su apogeo. Muchas implementaciones le permiten "enlazar" los subprocesos a los procesadores, mediante el uso de una variable de entorno definida por la implementación. Sin embargo, el usuario debe conocer la numeración del procesador y qué procesadores son "reales" frente a los de hipervínculo.
El problema es cómo OMP usa HT. ¡No es el ancho de banda de memoria! He intentado bucle simple en mi 2.6 GHz HT PIV. El resultado es increíble ...
Con OMP:
$ time ./a.out
4500000000
real 0m28.360s
user 0m52.727s
sys 0m0.064s
Sin OMP: $ time ./a.out 4500000000
real0 m25.417s
user 0m25.398s
sys 0m0.000s
Código:
#include <stdio.h>
#define U64 unsigned long long
int main() {
U64 i;
U64 N = 1000000000ULL;
U64 k = 0;
#pragma omp parallel for reduction(+:k)
for (i = 0; i < N; i++)
{
k += i%10; // last digit
}
printf ("%llu/n", k);
return 0;
}