¿Por qué un proceso completamente enlazado a la CPU funcionaría mejor con hyperthreading?


Dado:

  • Un trabajo completamente enlazado a la CPU muy grande (es decir, más de unos pocos ciclos de CPU), y
  • Una CPU con 4 núcleos físicos y un total de 8 lógicos,

¿Es posible que 8, 16 y 28 hilos funcionen mejor que 4 hilos? Mi entendimiento es que 4 hilos tendrían menores interruptores de contexto para realizar y tendrán menor sobrecarga en cualquier sentido que 8, 16 o 28 hilos tendrían en una máquina de 4 núcleos físicos. Sin embargo, el 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 get the timings se menciona en la sección 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 varios usuarios han proporcionado y la información proporcionada en los comentarios, finalmente puedo resumir la pregunta a lo que escribí anteriormente. Si la pregunta anterior le da el contexto completo, puede omitir la pregunta original a continuación.

Original Pregunta

¿Qué significa cuando decimos

Hyper-threading funciona duplicando ciertas secciones de la procesador - aquellos que almacenan el estado arquitectónico-pero no duplicando los principales recursos de ejecución. Esto permite un procesador hyper-threading para aparecer como el procesador "físico" habitual y un extra " lógico" procesador al sistema operativo host

?

Esta pregunta se hace hoy y se básicamente prueba el rendimiento de varios hilos haciendo 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 cortado algunas llaves para traer el código en una sola página para una legibilidad rápida.)

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 enlazado a la CPU. Sentí que hyper-threading podría contribuir a tal vez un aumento de velocidad del 30% (porque aquí tenemos tantas CPU hilos enlazados como los núcleos físicos (es decir, 4)). Pero casi alcanza un 64% de ganancia de rendimiento. Cuando ejecuté este código para 4 hilos, tomó aproximadamente 82 segundos y cuando ejecuté este código para 8, 16 y 28 hilos, 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 CPU era ~50% con 4 hilos. ¿No debería ser ~100%? Después de todo mi procesador tiene solo 4 núcleos físicos. Y el uso de CPU fue ~100% para 8 y 16 hilo.

Si alguien puede explicar el texto citado al principio, espero entender hyperthreading mejor con él y, a su vez, espero obtener la respuesta a ¿Por qué un proceso completamente enlazado a la CPU funcionaría mejor con hyperthreading?.


Para completar,

  • Tengo CPU Intel Core i7-4770 a 3.40 GHz, 3401 MHz, 4 Núcleos, 8 Procesadores lógicos.
  • Ejecuté el código en modo Release.
  • Sé que la forma en que los tiempos son medido es malo. Esto solo dará el 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 50% de uso de CPU cuando se ejecutan 4 subprocesos enlazados de CPU en una máquina de 4 núcleos físicos?
Author: Community, 2015-09-11

4 answers

Pude ver que el uso de CPU era ~50% con 4 hilos. No debería ser ~100%?

No, no debería.

¿Cuál es la justificación para el 50% de uso de CPU 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 de la utilización de la CPU en Windows (y en al menos algunos otros sistemas operativos también, por cierto). Una CPU HT aparece como dos núcleos para el sistema operativo, y se informa como tal.

Así, Windows ve un máquina de ocho núcleos, cuando tienes 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 completa de estos ocho núcleos.

Si solo está utilizando cuatro subprocesos, entonces estos subprocesos no pueden utilizar completamente los recursos de CPU disponibles y eso explica los tiempos. Pueden, a lo sumo, utilizar cuatro de los ocho núcleos disponibles y así, por supuesto, su la utilización máxima será del 50%. Una vez que pasa el número de núcleos lógicos (8), el tiempo de ejecución aumenta de nuevo; está agregando sobrecarga de programación sin agregar nuevos recursos computacionales en ese caso.


Por cierto...

HyperThreading ha mejorado bastante desde los viejos tiempos de la caché compartida y otras limitaciones, pero todavía 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 veo no más de un 20% de aceleración añadiendo los núcleos HT adicionales a un proceso con cuellos de botella computacionalmente.

 8
Author: Peter Duniho,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-12 20:03:18

Tubería de CPU

Cada instrucción tiene que pasar por varios pasos en la tubería para ser ejecutada completamente. Por lo menos, debe ser decodificado, enviado a la unidad de ejecución, luego realmente ejecutado 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 son no intercambiables: algunas operaciones solo se pueden hacer en una sola unidad de ejecución. Por ejemplo, las cargas de memoria son generalmente especializado en una o dos unidades, los almacenes de memoria se envían exclusivamente a otra unidad, todos los cálculos se realizan por algunas otras unidades.

Conociendo la canalización, podemos preguntarnos: ¿cómo puede funcionar la CPU tan rápido, si escribimos código puramente secuencial y cada instrucción tiene que pasar por tantas etapas de canalización? Aquí está la respuesta: procesador ejecuta instrucciones en fuera de orden moda. Tiene un gran búfer de reordenación (por ejemplo, para 200 instrucciones), y empuja muchas instrucciones a través de su tubería en paralelo. Si en cualquier momento alguna instrucción no puede ser ejecutada por cualquier motivo (espera los datos de la memoria lenta, depende de otra instrucción aún no terminada, lo que sea), entonces se retrasa por 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 las instrucciones retrasadas de ninguna manera.

Ahora podemos ver el problema de latencia. Incluso si un la instrucción está decodificada y todas sus entradas ya están disponibles, tomaría varios ciclos para ser ejecutada completamente. Este retraso se denomina latencia de instrucción. Sin embargo, sabemos que en este momento el procesador puede ejecutar muchas otras instrucciones independientes, si hay alguna.

Si una instrucción carga datos desde la caché L2, tiene que esperar unos 10 ciclos para que se carguen los datos. Si los datos se encuentran solo en RAM, entonces tomaría cientos de ciclos cargarlos en el procesador. En en este caso podemos decir que la instrucción tiene una alta latencia. Es importante para el máximo rendimiento tener algunas otras operaciones independientes para ejecutar en este momento. Esto a veces se llama latency hiding.

Al final, tenemos que admitir que la mayor parte del código real es secuencial en su naturaleza. Tiene algunas instrucciones independientes para ejecutar en paralelo, pero no demasiadas. No tener instrucciones para ejecutar causa burbujas de tubería, y conduce a ineficiente uso de 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.

P.D. Es posible que desee leer El manual de Agner Fog para comprender mejor el funcionamiento interno de las CPU modernas.

Hyper-threading

Cuando se ejecutan dos subprocesos en modo hyper-threading en un solo núcleo, el procesador puede intercalar sus instrucciones, lo que permite rellenar burbujas del primer hilo con instrucciones del segundo hilo. 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 una gran cantidad de accesos a la memoria, sino también en el código muy secuencial. Un código computacional bien optimizado puede utilizar completamente todos los recursos de la CPU, en cuyo caso verá no beneficio de HT (por ejemplo, dgemm rutina de BLAS bien optimizado).

P.d. Es posible que desee leer Intel explicación detallada de hyper-threading, incluyendo información sobre qué recursos se duplican o se comparten, y discusión sobre el rendimiento.

Conmutadores de contexto

El contexto es un estado interno de la CPU, que al menos incluye todos los registros. Cuando cambia el subproceso de ejecución, OS tiene que hacer un cambio de contexto (descripción detallada aquí). De acuerdo con esta respuesta , el cambio de contexto toma aproximadamente 10 microsegundos, mientras que el quant de tiempo del programador es 10 milisegundos o más (ver aquí ). Por lo tanto, los cambios de contexto no afectan mucho el tiempo total, porque rara vez se hacen lo suficiente. Tenga en cuenta que la competencia por cachés de CPU entre subprocesos puede aumentar el costo efectivo de los switches en algunos casos.

Sin embargo, en caso de hyper-threading cada núcleo tiene dos estados internamente: 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 se ejecutan 8 subprocesos en 4 físicos núcleo. Cuando ejecuta 16 subprocesos en quad-core, se realizan los cambios de contexto, 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 el funcionamiento interno de la tubería de CPU. Windows solo puede notar cuando un subproceso devuelve la ejecución al sistema operativo para: dormir, esperar a mutex, esperar a HDD y hacer otras cosas lentas. Como resultado, piensa que un núcleo se utiliza completamente si hay es un hilo que trabaja en él, que no duerme ni espera nada. Por ejemplo, puede comprobar que ejecutar endless loop while (true) {} conduce a la utilización completa de la CPU.

 6
Author: stgatilov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:52:59

No puedo explicar el gran volumen de aceleración que observaste: el 100% parece una mejora excesiva para Hyperthreading. Pero puedo explicar los principios en su lugar.

El principal beneficio de Hyperthreading es cuando un procesador tiene que cambiar entre subprocesos. Cada vez que hay más subprocesos que núcleos de CPU (verdadero 99.9997% del tiempo) y el sistema operativo decide cambiar a un subproceso diferente, tiene que realizar (la mayoría de) los siguientes pasos:

  1. Guardar el estado de la subproceso actual: esto incluye la pila, el estado de los registros, y el contador de programa. el lugar donde se guardan depende de la arquitectura, pero en términos generales se guardan en caché o en memoria. De cualquier manera, este paso toma tiempo.
  2. Ponga el Hilo en estado "Listo" (en lugar de estado "en ejecución").
  3. Cargue el estado del siguiente hilo: de nuevo, incluyendo la pila, los registros y el contador de programa, que una vez más, es un paso que toma time .
  4. Voltear el hilo en estado "en ejecución".

En una CPU normal (no-HT), el número de núcleos que tiene es la cantidad de unidades de procesamiento. Cada uno de ellos contiene registros, contadores de programa (registros), contadores de pila (registros), (generalmente) caché individual y unidades de procesamiento completas. Así que 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 está tomando demasiado tiempo y necesita esperar su turno para comenzar una vez más), la CPU necesita seguir esos cuatro pasos para descargar el subproceso y cargar el nuevo antes de que la ejecución del nuevo pueda 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 solo puede tener 4 subprocesos que se ejecutan simultáneamente, pero la CPU puede tener subprocesos "precargados" en el duplicado registros. Así que se están ejecutando 4 subprocesos, pero se cargan 8 subprocesos en la CPU, 4 activos, 4 inactivos. Luego, cuando es el momento de que la CPU cambie los hilos, en lugar de tener que realizar la carga/descarga en el momento en que los hilos necesitan cambiar, simplemente "alterna" qué hilo está activo y realiza la descarga/carga en segundo plano en los registros recién "inactivos". ¿Recuerdas los dos pasos que puse como sufijo "estos pasos toman tiempo"? En un sistema Hyperthreaded, los pasos 2 y 4 son los solo los que necesitan realizarse en tiempo real, mientras que los pasos 1 y 3 se realizan en segundo plano en el hardware (divorciados de cualquier concepto de subprocesos o procesos o núcleos de CPU).

Ahora, este proceso no acelera completamente el software 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 subproceso puede ser costosa. Incluso en entornos que no se ajustan a ese paradigma, puede haber beneficios de Hyperthreading.

Avísame si necesitas alguna aclaración. Han pasado algunos años desde CS250, así que puede que esté mezclando terminología aquí o allá; avísame si estoy usando los términos incorrectos para algo. Estoy 99.9997% seguro de que todo lo que estoy describiendo es exacto en términos de la lógica de cómo funciona todo.

 4
Author: Xirema,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-11 20:57:55

El Hyper-threading funciona intercalando instrucciones en la canalización de ejecución del procesador. Mientras el procesador realiza operaciones de lectura y escritura en un "subproceso", realiza una evaluación lógica en el otro "subproceso", manteniéndolos separados y dándole una percepción de duplicación en el rendimiento.

La razón por la que obtienes una aceleración tan grande es porque no hay lógica de ramificación en tu método DoWork. Todo es un gran bucle con una secuencia de ejecución muy predecible.

Un procesador la canalización de ejecución 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 realmente un salto condicional (como una instrucción if), esto es una mala noticia, porque el procesador tiene que vaciar toda la canalización y obtener instrucciones de una parte diferente de la memoria.

Usted puede encontrar que si usted pone if declaraciones en su DoWork método, no obtendrá 100% speedup...

 3
Author: Steztric,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-11 20:54:07