c# - juegos - hyper threading que es
¿Por qué un proceso totalmente enlazado a la CPU funcionaría mejor con hyperthreading? (4)
Pude ver que el uso de la CPU fue de ~ 50% con 4 subprocesos. ¿No debería ser ~ 100%?
No, no debería.
¿Cuál es la justificación para el uso de CPU del 50% cuando se ejecutan 4 subprocesos enlazados de CPU en una máquina de 4 núcleos físicos?
Esto es simplemente cómo se informa la utilización de la CPU en Windows (y, por cierto, también en otros sistemas operativos). Una CPU HT se muestra como dos núcleos para el sistema operativo, y se informa como tal.
Por lo tanto, Windows ve una máquina de ocho núcleos, cuando tiene cuatro CPU HT. Verá ocho gráficos de CPU diferentes si observa la pestaña "Rendimiento" en el Administrador de tareas, y la utilización total de la CPU se calcula con una utilización del 100% que es la utilización total de estos ocho núcleos.
Si solo está utilizando cuatro subprocesos, estos subprocesos no pueden utilizar completamente los recursos de CPU disponibles y eso explica los tiempos . Como máximo, pueden usar cuatro de los ocho núcleos disponibles, por lo que, por supuesto, su utilización será máxima en un 50%. Una vez que se pasa el número de núcleos lógicos (8), el tiempo de ejecución aumenta nuevamente; está agregando una sobrecarga de programación sin agregar ningún nuevo recurso computacional en ese caso.
Por cierto…
HyperThreading ha mejorado bastante con respecto a los viejos tiempos de la memoria caché compartida y otras limitaciones, pero aún así nunca proporcionará el mismo beneficio de rendimiento que una CPU completa podría, ya que sigue habiendo cierta contención dentro de la CPU. Así que incluso ignorando la sobrecarga del sistema operativo, su mejora del 35% en la velocidad me parece bastante buena. A menudo no veo más de un 20% de aceleración agregando los núcleos HT adicionales a un proceso de cuello de botella computacional.
Dado:
- Un trabajo de CPU muy grande (es decir, más que unos pocos ciclos de CPU), y
- Una CPU con 4 núcleos físicos y 8 lógicos en total,
¿es posible que 8, 16 y 28 hilos tengan un mejor rendimiento que 4 hilos? Tengo entendido que 4 subprocesos tendrían menos cambios de contexto para realizar y tendrán menos sobrecarga en cualquier sentido que 8, 16 o 28 subprocesos tendrían en una máquina de 4 núcleos físicos . Sin embargo, los tiempos son -
Threads Time Taken (in seconds)
4 78.82
8 48.58
16 51.35
28 52.10
El código utilizado para probar los tiempos se menciona en la sección de la pregunta original a continuación. Las especificaciones de la CPU también se dan en la parte inferior.
Después de leer las respuestas que han proporcionado varios usuarios y la información que se proporciona en los comentarios, finalmente puedo resumir la pregunta a lo que escribí anteriormente. Si la pregunta anterior le proporciona el contexto completo, puede omitir la pregunta original a continuación.
Pregunta original
¿Qué significa cuando decimos?
Hyper-threading funciona duplicando ciertas secciones del procesador, aquellas que almacenan el estado arquitectónico, pero no duplicando los principales recursos de ejecución. Esto permite que un procesador de subprocesamiento aparezca como el procesador "físico" habitual y un procesador "lógico" adicional para el sistema operativo host.
?
Esta pregunta se hace hoy en SO y básicamente prueba el rendimiento de varios subprocesos que realizan el mismo trabajo. Tiene el siguiente código:
private static void Main(string[] args)
{
int threadCount;
if (args == null || args.Length < 1 || !int.TryParse(args[0], out threadCount))
threadCount = Environment.ProcessorCount;
int load;
if (args == null || args.Length < 2 || !int.TryParse(args[1], out load))
load = 1;
Console.WriteLine("ThreadCount:{0} Load:{1}", threadCount, load);
List<Thread> threads = new List<Thread>();
for (int i = 0; i < threadCount; i++)
{
int i1 = i;
threads.Add(new Thread(() => DoWork(i1, threadCount, load)));
}
var timer = Stopwatch.StartNew();
foreach (var thread in threads) thread.Start();
foreach (var thread in threads) thread.Join();
timer.Stop();
Console.WriteLine("Time:{0} seconds", timer.ElapsedMilliseconds/1000.0);
}
static void DoWork(int seed, int threadCount, int load)
{
var mtx = new double[3,3];
for (var i = 0; i < ((10000000 * load)/threadCount); i++)
{
mtx = new double[3,3];
for (int k = 0; k < 3; k++)
for (int l = 0; l < 3; l++)
mtx[k, l] = Math.Sin(j + (k*3) + l + seed);
}
}
(He recortado algunos tirantes para llevar el código en una sola página para facilitar la lectura).
Ejecuté este código en mi máquina para replicar el problema. Mi máquina tiene 4 núcleos físicos y 8 lógicos. El método DoWork()
en el código anterior está completamente vinculado a la CPU. Sentí que el hyper-threading podría contribuir a una aceleración del 30% (porque aquí tenemos tantos hilos enlazados a la CPU como los núcleos físicos (es decir, 4)). Pero casi alcanza el 64% de ganancia de rendimiento. Cuando ejecuté este código para 4 subprocesos, tardó unos 82 segundos y cuando ejecuté este código para 8, 16 y 28 subprocesos, se ejecutó en todos los casos en aproximadamente 50 segundos.
Para resumir los tiempos:
Threads Time Taken (in seconds)
4 78.82
8 48.58
16 51.35
28 52.10
Pude ver que el uso de la CPU fue de ~ 50% con 4 subprocesos. ¿No debería ser ~ 100%? Después de todo mi procesador tiene solo 4 núcleos físicos. Y el uso de la CPU fue ~ 100% para 8 y 16 hilos.
Si alguien puede explicar el texto citado al principio, espero entender mejor el subprocesamiento con él y, a su vez, espero obtener la respuesta a ¿Por qué un proceso totalmente vinculado a la CPU funcionaría mejor con el subproceso? .
Por el bien de la finalización,
- Tengo una CPU Intel Core i7-4770 a 3.40 GHz, 3401 MHz, 4 Core (s), 8 Logical Processor (s).
- Corrí el código en modo Release.
- Sé que la forma en que se miden los tiempos es mala. Esto solo dará tiempo para el hilo más lento. Tomé el código tal como es de la otra pregunta. Sin embargo, ¿cuál es la justificación para el uso de CPU del 50% cuando se ejecutan 4 subprocesos enlazados de CPU en una máquina de 4 núcleos físicos?
Tubería de CPU
Cada instrucción debe pasar por varios pasos en la pipeline para ejecutarse completamente. Como mínimo, debe decodificarse, enviarse a la unidad de ejecución y luego ejecutarse allí. Hay varias unidades de ejecución en las CPU modernas, y pueden ejecutar instrucciones completamente en paralelo. Por cierto, las unidades de ejecución no son intercambiables: algunas operaciones solo pueden realizarse en una sola unidad de ejecución. Por ejemplo, las cargas de memoria suelen estar especializadas en una o dos unidades, los almacenes de memoria se envían exclusivamente a otra unidad, todos los cálculos se realizan mediante otras unidades.
Conociendo la tubería, podemos preguntarnos: ¿cómo puede la CPU trabajar tan rápido, si escribimos código puramente secuencial y cada instrucción tiene que pasar por tantas etapas de tubería? Aquí está la respuesta: el procesador ejecuta las instrucciones out-of-order manera desordenada. Tiene un gran búfer de reorden (por ejemplo, para 200 instrucciones), y empuja muchas instrucciones a través de su tubería en paralelo. Si en algún momento alguna instrucción no puede ejecutarse por algún motivo (espera datos de una memoria lenta, depende de otra instrucción que aún no haya finalizado, cualquiera que sea), entonces se retrasa algunos ciclos. Durante este tiempo, el procesador ejecuta algunas instrucciones nuevas, que se encuentran después de las instrucciones retrasadas en nuestro código, dado que no dependen de ninguna manera de las instrucciones retrasadas.
Ahora podemos ver el problema de la latency . Incluso si una instrucción se decodifica y todas sus entradas ya están disponibles, se necesitarían varios ciclos para ejecutarse completamente. Este retraso se llama latencia de instrucción. Sin embargo, sabemos que en este momento el procesador puede ejecutar muchas otras instrucciones independientes, si las hay.
Si una instrucción carga datos del caché L2, tiene que esperar unos 10 ciclos para que se carguen los datos. Si los datos se encuentran solo en la memoria RAM, se necesitarían cientos de ciclos para cargarlos en el procesador. En este caso podemos decir que la instrucción tiene una alta latencia. Es importante para el máximo rendimiento tener otras operaciones independientes que ejecutar en este momento. Esto a veces se llama ocultamiento de latencia .
Al final, tenemos que admitir que la mayor parte del código real es de naturaleza secuencial. Tiene algunas instrucciones independientes para ejecutar en paralelo, pero no demasiadas. No tener instrucciones para ejecutar causa burbujas en la tubería y conduce a un uso ineficiente de los transistores del procesador. Por otro lado, las instrucciones de dos hilos diferentes son automáticamente independientes en casi todos los casos. Esto nos lleva directamente a la idea de hyper-threading.
Es posible que desee leer el manual de Agner Fog para comprender mejor las partes internas de las CPU modernas.
Hyper-threading
Cuando dos subprocesos se ejecutan en el modo de subprocesamiento en un solo núcleo, el procesador puede intercalar sus instrucciones, lo que permite rellenar burbujas desde el primer subproceso con instrucciones del segundo subproceso. Esto permite utilizar mejor los recursos del procesador, especialmente en el caso de programas ordinarios. Tenga en cuenta que HT puede ayudar no solo cuando tiene muchos accesos de memoria, sino también en código muy secuencial. Un código computacional bien optimizado puede utilizar completamente todos los recursos de la CPU, en cuyo caso no verá ningún beneficio de HT (por ejemplo, la rutina dgemm
de BLAS bien optimizado).
Es posible que desee leer la explicación detallada de Intel sobre hipervínculos , incluida la información sobre qué recursos se duplican o comparten, y la discusión sobre el rendimiento.
Cambios de contexto
El contexto es un estado interno de la CPU, que al menos incluye todos los registros. Cuando la secuencia de ejecución cambia, el sistema operativo tiene que hacer un cambio de contexto (descripción detallada here ). De acuerdo con esta respuesta , el cambio de contexto toma alrededor de 10 microsegundos, mientras que la cantidad de tiempo del programador es de 10 milisegundos o más (consulte here ). Por lo tanto, los cambios de contexto no afectan mucho el tiempo total, porque se realizan con poca frecuencia. Tenga en cuenta que, en algunos casos, la competencia por los cachés de CPU entre subprocesos puede aumentar el costo efectivo de los switches.
Sin embargo, en el caso de un subproceso, cada núcleo tiene dos estados internos: dos conjuntos de registros, cachés compartidos, un conjunto de unidades de ejecución. Como resultado, el sistema operativo no tiene necesidad de hacer ningún cambio de contexto cuando ejecuta 8 hilos en 4 núcleos físicos. Cuando ejecuta 16 subprocesos en quad-core, los cambios de contexto se realizan, pero toman una pequeña parte del tiempo total, como se explicó anteriormente.
Gestor de procesos
Hablando de la utilización de la CPU que se ve en el administrador de procesos, no mide las partes internas de la tubería de la CPU. Windows solo puede notar cuando un subproceso devuelve la ejecución al sistema operativo para: dormir, esperar mutex, esperar a HDD y hacer otras cosas lentas. Como resultado, piensa que un núcleo se utiliza por completo si hay un hilo trabajando en él, que no duerme ni espera nada. Por ejemplo, puede verificar que la ejecución del bucle sin fin while (true) {}
conduce a la utilización completa de la CPU.
Hyper-threading funciona intercalando instrucciones en la tubería de ejecución del procesador. Mientras el procesador está realizando operaciones de lectura-escritura en un ''subproceso'', está realizando una evaluación lógica en el otro ''subproceso'', manteniéndolos separados y dándole una duplicación en el rendimiento.
La razón por la que obtienes una gran aceleración es porque no hay una lógica de bifurcación en tu método DoWork
. Es todo un gran bucle con una secuencia de ejecución muy predecible.
Una tubería de ejecución del procesador tiene que pasar por varios ciclos de reloj para ejecutar un solo cálculo. El procesador intenta optimizar el rendimiento cargando previamente el búfer de ejecución con las siguientes instrucciones. Si la instrucción cargada es en realidad un salto condicional (como una instrucción if
), esto es una mala noticia, porque el procesador debe vaciar todo el flujo y obtener instrucciones de una parte diferente de la memoria.
Puede encontrar que si coloca declaraciones if
en su método DoWork
, no obtendrá un 100% de aceleración ...
No puedo explicar el gran volumen de aceleración que observó: el 100% parece una mejora demasiado grande para Hyperthreading. Pero puedo explicar los principios en su lugar.
El principal beneficio de Hyperthreading es cuando un procesador tiene que cambiar entre subprocesos. Siempre que haya más subprocesos que núcleos de CPU (el 99,9997% de las veces) y el sistema operativo decide cambiar a un subproceso diferente, debe realizar (la mayoría de) los siguientes pasos:
- Guarde el estado del hilo actual: esto incluye la pila, el estado de los registros y el contador del programa. donde se guardan depende de la arquitectura, pero en general se guardarán en caché o en memoria. De cualquier manera, este paso lleva tiempo .
- Ponga el hilo en el estado "Listo" (a diferencia del estado "En ejecución").
- Cargue el estado del siguiente hilo: nuevamente, incluyendo la pila, los registros y el contador del programa, que una vez más, es un paso que lleva tiempo .
- Voltear el hilo en estado "Corriendo".
En una CPU normal (no HT), el número de núcleos que tiene es la cantidad de unidades de procesamiento. Cada uno de estos contiene registros, contadores de programa (registros), contadores de pila (registros), (generalmente) caché individual y unidades de procesamiento completas. Entonces, si una CPU normal tiene 4 núcleos, puede ejecutar 4 subprocesos simultáneamente. Cuando se realiza un subproceso (o el sistema operativo ha decidido que se está demorando demasiado y debe esperar su turno para comenzar de nuevo), la CPU debe seguir esos cuatro pasos para descargar el subproceso y cargar el nuevo antes de ejecutar el Una nueva puede comenzar.
En una CPU HyperThreading, por otro lado, lo anterior es cierto, pero además, cada núcleo tiene un conjunto duplicado de registros, contadores de programas, contadores de pila y (a veces) caché . Lo que esto significa es que una CPU de 4 núcleos todavía puede tener solo 4 subprocesos ejecutándose simultáneamente, pero la CPU puede tener subprocesos "precargados" en los registros duplicados . Por lo tanto, se están ejecutando 4 subprocesos, pero se cargan 8 subprocesos en la CPU, 4 activos, 4 inactivos. Luego, cuando es el momento para que la CPU cambie los subprocesos, en lugar de tener que realizar la carga / descarga en el momento en que los subprocesos necesitan cambiar, simplemente "alterna" qué subproceso está activo y realiza la descarga / carga en segundo plano en los nuevos registros "inactivos". ¿Recuerdas los dos pasos que he marcado con "estos pasos llevan tiempo"? En un sistema Hyperthreaded, los pasos 2 y 4 son los únicos que deben realizarse en tiempo real, mientras que los pasos 1 y 3 se realizan en segundo plano en el hardware (separado de cualquier concepto de subprocesos o procesos o núcleos de CPU).
Ahora, este proceso no acelera completamente el software de multiproceso, pero en un entorno donde los subprocesos a menudo tienen cargas de trabajo extremadamente pequeñas que realizan con mucha frecuencia, la cantidad de interruptores de subprocesos puede ser costosa. Incluso en entornos que no se ajustan a ese paradigma, puede haber beneficios de Hyperthreading.
Déjame saber si necesitas alguna aclaración. Han pasado algunos años desde CS250, así que puedo estar confundiendo la terminología aquí o allá; Avísame si estoy usando los términos equivocados para algo. Estoy seguro al 99.9997% de que todo lo que describo es exacto en términos de la lógica de cómo funciona todo.