3 puntos por GN⁺ 2024-07-29 | 1 comentarios | Compartir por WhatsApp
  • SMT es una técnica en la que un solo núcleo de CPU emite instrucciones de varios hilos en el mismo ciclo para llenar mejor los recursos de ejecución que quedarían ociosos si solo se dependiera del paralelismo a nivel de instrucciones
  • El Hyper-Threading de Intel implementa dos hilos por núcleo y duplica el estado arquitectónico, de modo que para el sistema operativo un núcleo físico se ve como dos procesadores lógicos
  • La implementación real no simplemente duplica todos los recursos: duplica el puntero de instrucción, el ITLB y el RAT; comparte la trace cache y el TLB; y divide equitativamente la cola de uops, los registros físicos y el reorder buffer
  • La ganancia de rendimiento varía según la carga de trabajo: si dos hilos compiten agresivamente por la caché, puede volverse más lento; si son hilos cooperativos que intercambian datos, pueden acelerarse gracias a la caché compartida
  • Los recursos compartidos y la ejecución especulativa pueden derivar en vulnerabilidades de seguridad, así que en entornos sensibles o en cargas que exigen máximo rendimiento y mínima latencia, desactivar SMT puede ser una opción realista

Por qué se necesita SMT

  • Los procesadores modernos cuentan con cientos de registros, múltiples unidades de carga/almacenamiento y unidades aritméticas, y para aprovecharlos usan técnicas de paralelismo a nivel de instrucciones como pipeline, arquitecturas superscalar y ejecución fuera de orden
  • Un pipeline divide la ejecución de instrucciones en varias etapas y en cada ciclo pasa una instrucción a la siguiente etapa; en un pipeline de profundidad 5, después del quinto ciclo pueden estar avanzando hasta 5 instrucciones al mismo tiempo
  • Un procesador superscalar puede emitir varias instrucciones en un ciclo, y los procesadores Intel Core i7 recientes pueden emitir 4 instrucciones por ciclo
  • En programas reales es difícil encontrar suficientes instrucciones independientes, así que hay momentos en que los recursos de ejecución quedan ociosos
    • El desperdicio horizontal ocurre cuando dentro de un mismo hilo no se encuentran suficientes instrucciones independientes para llenar el ancho de emisión
    • El desperdicio vertical ocurre cuando las siguientes instrucciones dependen de una que ya está en ejecución y no se puede emitir ninguna instrucción en un ciclo
  • El multithreading tradicional puede reducir el desperdicio vertical al emitir en cada ciclo instrucciones de un solo hilo y cambiar a otro hilo en el siguiente ciclo, pero el desperdicio horizontal y el overhead de cambio de contexto siguen presentes
  • SMT emite instrucciones de varios hilos en el mismo ciclo sin cambio de contexto, llenando los recursos de ejecución con una utilización mayor
  • La implementación SMT de Intel, Hyper-Threading, está limitada a dos hilos por núcleo

Estructura básica del SMT al estilo Intel

  • Un procesador normal sin SMT solo puede ejecutar instrucciones de un hilo a la vez
  • Cada hilo tiene un estado arquitectónico que incluye valores de registros, contador de programa, registros de control y más
  • Para ejecutar al mismo tiempo instrucciones de dos hilos, hay que representar simultáneamente el estado de ambos, por lo que una implementación SMT duplica el estado arquitectónico del procesador
  • Debido a esta duplicación, un solo procesador físico aparece ante el sistema operativo como dos procesadores lógicos, y el sistema operativo puede planificar hilos en cada uno
  • Los buffers y recursos de ejecución a nivel microarquitectónico se duplican, comparten o dividen según factores como costo, consumo eléctrico y área del chip
  • El análisis se centra sobre todo en la implementación SMT de Intel y se basa en un white paper de Intel de 2002

Las tres partes de la microarquitectura de CPU

  • El procesador expone al programador la ISA como interfaz pública, y la ISA incluye el conjunto de instrucciones y los registros que esas instrucciones pueden usar
  • La microarquitectura es la implementación interna, que puede variar incluso entre modelos de procesador que soportan la misma ISA
  • La microarquitectura de los procesadores modernos se divide en tres grandes partes
    • Frontend: incluye la unidad de control de instrucciones que trae y decodifica las instrucciones del programa que se ejecutarán después
    • Backend: incluye recursos de ejecución como registros físicos, unidades aritméticas y unidades de carga/almacenamiento, y asigna recursos a las instrucciones decodificadas para programar su ejecución
    • retirement unit: refleja finalmente los resultados de las instrucciones ejecutadas en el estado arquitectónico del procesador

SMT en el frontend

  • El puntero de instrucción rastrea la dirección de la siguiente instrucción a traer
    • Un procesador con SMT tiene dos conjuntos de punteros de instrucción para seguir de forma independiente las siguientes instrucciones de dos programas
  • La trace cache guarda trazas de instrucciones decodificadas recientemente para reducir el costo de decodificación y la latencia de ejecución de instrucciones que se repiten
    • Los dos procesadores lógicos la comparten dinámicamente según necesidad
    • Si un hilo ejecuta más instrucciones, puede ocupar más entradas de la trace cache
    • Cada entrada está etiquetada con información del hilo para distinguir instrucciones de ambos hilos
    • El acceso a la trace cache se arbitra en cada ciclo entre los dos procesadores lógicos
  • Si ocurre un trace cache miss, el frontend busca las instrucciones en la L1 instruction cache de esa dirección, y si también hay un miss en la L1 instruction cache, debe traerlas desde el siguiente nivel de caché o desde la memoria principal
  • La L1 instruction cache almacena datos con direcciones virtuales, pero para acceder a la memoria principal se necesitan direcciones físicas
  • El ITLB convierte direcciones virtuales en físicas al mantener las traducciones recientes
    • En un procesador con SMT, cada procesador lógico tiene su propia caché ITLB
    • La lógica que trae instrucciones desde memoria principal funciona como first come first served, pero reserva al menos un slot de solicitud para cada procesador lógico, de modo que ambos puedan avanzar
    • Las instrucciones que llegan desde la memoria principal se guardan antes de decodificarse en un pequeño streaming buffer, y en un procesador con SMT este buffer también está duplicado por procesador lógico
  • Después de traerse, las instrucciones se decodifican en uops más pequeñas y simples
    • Las uops entran en la cola de uops, que sirve como frontera entre el frontend y el backend de la CPU
    • La cola de uops se comparte de manera equitativa entre los dos procesadores lógicos, y esta división estática permite que ambos avancen de forma independiente

SMT en el backend

  • El backend toma microinstrucciones de la cola de uops y las ejecuta, pero no se limita al orden original del programa y realiza ejecución fuera de orden
  • Las instrucciones cercanas en el programa suelen depender entre sí, y si hay operaciones de alta latencia como una lectura desde memoria principal, las instrucciones dependientes también deben esperar
  • El motor de ejecución fuera de orden reduce el desperdicio de recursos al ejecutar instrucciones posteriores antes que las anteriores en el orden original
  • El allocator identifica los recursos necesarios para cada microinstrucción y los asigna según disponibilidad
    • En un ciclo asigna recursos a las microinstrucciones de un procesador lógico, y en el siguiente cambia al otro procesador lógico
    • Si en la cola de uops solo hay microinstrucciones de un procesador lógico, o si uno de ellos ya agotó todos sus recursos asignados, el allocator usa todos los ciclos para el otro procesador lógico
  • Los principales recursos del backend mezclan duplicación, compartición y partición
    • A nivel ISA, X86-64 solo tiene 16 registros enteros de propósito general, pero a nivel microarquitectónico hay cientos de registros físicos enteros y una cantidad similar de registros de punto flotante
    • En un procesador con SMT, los registros físicos se dividen equitativamente entre los dos procesadores lógicos
    • Los load buffer y store buffer usados para operaciones de lectura y escritura en memoria también se dividen equitativamente entre ambos procesadores lógicos

Renombrado de registros, scheduling y commit

  • Para la ejecución fuera de orden, el backend realiza renombrado de registros
    • A nivel ISA hay pocos registros arquitectónicos, por lo que las instrucciones del programa reutilizan el mismo registro en múltiples instrucciones independientes
    • El motor de ejecución fuera de orden reemplaza el registro lógico original por uno de los registros físicos para permitir ejecución más temprana y en paralelo
    • Ese mapeo se mantiene en la register alias table, o RAT
    • Como los dos procesadores lógicos tienen cada uno su propio conjunto de registros arquitectónicos, el RAT también tiene una copia para cada uno
  • Las instrucciones que pasan por register renaming y por la etapa de allocator entran en colas de preparación
    • Una cola es para instrucciones de lectura/escritura de memoria y la otra para instrucciones generales
    • En un núcleo con SMT, estas colas se dividen equitativamente entre los dos procesadores lógicos
  • El procesador tiene varios instruction scheduler funcionando en paralelo
    • En cada ciclo de CPU, parte de las instrucciones de las colas de preparación se envía a los scheduler
    • Las colas envían instrucciones de un procesador lógico en un ciclo y del otro en el siguiente
    • El scheduler no presta atención al procesador lógico y envía de inmediato a ejecución las microinstrucciones cuyos operandos y unidades de ejecución necesarias ya están listas
    • Para mantener la equidad, se limita la cantidad de entradas activas que un procesador lógico puede tener dentro de la scheduler queue
  • Los resultados de las instrucciones ya ejecutadas entran en el reorder buffer
    • Aunque las instrucciones se ejecuten fuera de orden, deben hacer commit al estado arquitectónico del procesador en el orden original del programa
    • En un núcleo con SMT, el reorder buffer se divide equitativamente entre los dos procesadores lógicos
  • La retirement unit sigue si las instrucciones están listas para hacer commit al estado arquitectónico y las retira en el orden correcto del programa
    • En un núcleo con SMT, alterna entre las microinstrucciones de cada procesador lógico
    • Si un procesador lógico no tiene microinstrucciones para retirar, todo el ancho de banda se usa para el otro procesador lógico
    • Después de retirar una instrucción, puede ser necesario escribir en la L1 cache, y esta lógica de selección de escritura también alterna en cada ciclo entre los dos procesadores lógicos

Subsistema de memoria e impacto de la caché

  • El TLB, que convierte las direcciones virtuales de solicitudes de datos en direcciones físicas, es compartido dinámicamente entre los dos procesadores lógicos según necesidad
  • Las entradas del TLB están etiquetadas con el logical processor id para distinguir los elementos de ambos procesadores lógicos
  • Cada núcleo de CPU tiene su propia L1 cache privada
  • La L2 cache puede ser privada o compartida entre núcleos según la microarquitectura
  • Si existe una L3 cache, se comparte entre núcleos
  • La caché no es consciente de la existencia de procesadores lógicos
  • Como la L1 cache y en algunos casos también la L2 cache son privadas del núcleo, pueden contener juntos los datos de ambos procesadores lógicos según necesidad
    • Si dos hilos usan la caché de forma agresiva, puede haber conflictos de datos y eviction, lo que degrada el rendimiento
    • Si dos hilos trabajan sobre el mismo conjunto de datos, la caché compartida puede mejorar el rendimiento

Criterios de decisión en rendimiento y seguridad

  • Incluso si en un núcleo con SMT solo se ejecuta un hilo, muchos buffers y recursos de ejecución siguen compartidos o divididos entre los dos procesadores lógicos, lo que puede reducir el rendimiento potencial de un solo hilo
  • En un procesador lógico sin uso, el sistema operativo ejecuta un idle loop, y ese loop también puede consumir recursos que el otro procesador lógico podría usar para alcanzar el máximo rendimiento
  • En los procesadores Intel Core, cuando solo se ejecuta un hilo en el núcleo parece que no hay compartición ni partición de recursos, y Intel lo presenta como una mejora introducida en esa generación
  • Cuando dos hilos se ejecutan en los dos procesadores lógicos de un núcleo con SMT, el patrón de acceso a caché determina el rendimiento
    • Si ambos hilos compiten por la caché, expulsan los datos del otro y el rendimiento cae
    • Si cooperan, por ejemplo cuando uno consume datos producidos por el otro, compartir datos en caché puede mejorar el rendimiento
    • Si no compiten de forma agresiva por la caché, pueden aumentar la utilización de los recursos del núcleo sin perjudicarse entre sí
  • Muchos especialistas consideran que, para programas que necesitan el máximo rendimiento absoluto, conviene desactivar SMT para que un solo hilo pueda usar todos los recursos
  • SMT también trae problemas de seguridad
    • Debido a los recursos compartidos y la ejecución especulativa, puede abrirse la posibilidad de que datos sensibles se filtren hacia un atacante
    • La documentación de Oracle Linux y Red Hat enlaza a ejemplos de problemas de seguridad relacionados con SMT
    • La recomendación general apunta a desactivar SMT en el sistema
    • Incluso hay rumores de que Intel podría eliminar Hyper-Threading en Arrow Lake, su próxima generación de procesadores

Referencias

1 comentarios

 
GN⁺ 2024-07-29
Comentarios en Hacker News
  • Si se entiende SMT de forma muy simplificada, queda claro que mientras un hilo está detenido por un cache miss, se puede mantener ocupada la valiosa ALU
    En laptops antiguas, la LPDDR era más lenta y había menos núcleos, así que probablemente valía más la pena, pero hoy a veces hay más núcleos que trabajo escalable, así que se siente menos útil
    También se evita la contención de caché al no colocar trabajo en el mismo núcleo que un hilo importante, porque se sabe que el cuello de botella es el rendimiento de un solo hilo
    Hace tiempo probé núcleos Efficient/Performance y núcleos SMT en renderizado multihilo de DirectX 12, y en un i7-12700K los tiempos de render en escenas complejas eran casi iguales usando solo P-core, P+SMT, o P+E+SMT. En cambio, en Xbox Series X la misma prueba fue un poco más rápida cuando también se asignó trabajo a SMT

    • El renderizado siempre fue uno de esos escenarios donde SMT daba igual o era más lento. Ya hay mucha operación matemática, así que la FPU siempre está ocupada, y en particular el divisor es una de las unidades más costosas del procesador
      SMT brilla cuando se espera E/S o se hacen tareas simples de enteros. Si ambos hilos pueden saturar la FPU, por lo general SMT termina siendo más lento por el etiquetado adicional necesario para marcar la pertenencia de los datos internos del CPU
    • El Hyper-Threading de Intel es básicamente un hack del pipeline de escritura
      El punto clave no es tanto el cache miss, sino permitir que el núcleo ejecute otra cosa mientras se completan las escrituras
      Por eso algunos códigos no escalan bien, mientras otros logran mejoras de velocidad casi lineales
    • Hoy en día, especialmente considerando incluso la alimentación trasera de energía, me pregunto cuánto reduce una parada de caché de un procesador el thermal throttling de ese procesador y de los vecinos
      Tal vez convenga simplemente dejar que estos procesadores se echen una siestita de vez en cuando
    • Respecto a la idea de que LPDDR antes era más lenta, curiosamente la latencia no ha mejorado mucho. La latencia CAS de DDR2/3/4/5 suele estar en el rango de 5~10 ns
      Han mejorado el ancho del bus, las transferencias por segundo, el queueing y la energía por bit transferido/almacenado, pero si el programa necesita datos que no están en caché y además falla la predicción, al final el problema sigue siendo la latencia de la RAM
    • Me pregunto si, en vez de SMT, se podría apagar por momentos la ALU/FPU que no se usa mientras se espera algo en la parte frontal del pipeline, para ir más por reducir calor y consumo eléctrico que por maximizar la utilización
  • Se dice que los CPU Arrow Lake de próxima generación de Intel eliminarán por completo Hyper-Threading, es decir, SMT
    Como la ganancia de rendimiento siempre dependió mucho de la aplicación, quizá simplificar sea mejor
    Hay una discusión reciente sobre cuándo y dónde tiene sentido aquí: https://news.ycombinator.com/item?id=39097124

    • La mayoría de los programas llega a un límite en la cantidad de hilos que puede usar razonablemente. Cuando hay muchos menos núcleos que eso, SMT tiene sentido para aprovechar mejor los recursos del CPU, pero una vez que hay suficientes núcleos, SMT puede dejar de tener sentido
      Aún no estoy seguro de que ya hayamos llegado necesariamente a ese punto, pero los P/E cores de Intel son una alternativa con un objetivo parecido, y en desktops con mucha carga de tareas de uno o pocos hilos suenan bastante razonables. También parece valioso no tener que lidiar en la optimización de aplicaciones con la distinción entre SMT y E-core
      En cambio, AMD planea por ahora mantener núcleos mayormente homogéneos y seguir usando SMT. Qué estrategia es realmente mejor parece variar mucho según la aplicación, así que no es fácil juzgarlo de forma simple
    • En mi caso de uso personal, haciendo juegos y motores, me fue más rápido rezarle al dios de la planificación de hilos del CPU para que cada hilo usara su propio núcleo que confiar en Hyper-Threading
      Por eso decidí limitar la cantidad de hilos a std::thread::hardware_concurrency() / 2 - 1, es decir, número de núcleos - 1. Estoy trabajando con std::vector
    • Según los benchmarks comunes en la industria, el Hyper-Threading de Intel al menos una vez cada dos generaciones fue más lento que desactivarlo
      Incluso cuando funcionaba bien, la mejora apenas llegaba a porcentajes de dos dígitos, y hubo períodos en generaciones seguidas donde fue peor, así que no sé por qué lo siguen intentando
    • ¿Eso también aplica a las piezas para servidores?
  • Cada vez que leo cómo funcionan estas funciones de CPU de bajo nivel me sorprendo
    En la universidad tomé una materia que sería algo como “Introducción al hardware de computadoras”, pero en realidad deberían haberla llamado “Introducción al diseño de CPU”. Construíamos sumadores, latches y flip-flops con compuertas lógicas, y al final del semestre podíamos diseñar un procesador muy básico a nivel de compuertas
    Pero me cuesta imaginar cómo se llega a inventar cosas como el renombrado de registros o la ejecución fuera de orden. ¿Eso también se diseña a nivel de compuertas? ¿O existe un “compilador” que, a partir del lenguaje usado, acomoda compuertas y transistores?

    • Tomé la materia del siguiente nivel, y ahí aprendimos SMT y algunas cosas más
      Todas las tareas las hicimos en Verilog, un lenguaje de descripción de hardware, y gracias a eso se podían abstraer varios elementos al escribir
  • Uno de los grandes malentendidos comunes de los usuarios sobre SMT es el modelo mental de imaginar que hay un “núcleo real” y otro núcleo inferior
    En todos los aspectos observables, ambos hilos son equivalentes

    • Creo que esa percepción viene del rendimiento. Los dos hilos pueden ejecutar el mismo trabajo, pero no se obtiene el doble de rendimiento como sí pasaría con un segundo hilo “real”, es decir, un segundo núcleo
      Al final, si uno mira solo el rendimiento, conceptualmente se parece más a tener como 1.25 núcleos de un solo hilo, o esa proporción según la aplicación
    • Cuando corres una tarea muy optimizada y pesada en cómputo, como compresión de video, los ventiladores de la computadora rugen como motor de avión, pero el administrador de tareas muestra 50% de uso de CPU, así que es entendible que se forme esa idea
    • Los CPU nuevos de Intel sí tienen realmente tanto “P-core”, que sería un núcleo real, como “E-core”, que sería un núcleo inferior
      Creo que la razón principal para introducir E-core no fue el consumo eléctrico ni el rendimiento, sino el calor y el área del die. Por eso siempre compro chips sin E-core; me parecen mejores
  • Me pregunto cómo se busca este tipo de artículos técnicos detallados.
    Busqué justo este tema, pero como era de esperarse, al estar orientado a usuarios finales, en los resultados solo aparecieron artículos para usuarios que no explicaban nada bien.

    • Basta con usar https://hn.algolia.com. Eso asumiendo que la mayoría de los textos de este tipo se publican o al menos se mencionan en HN.
    • Me parece que los LLM con acceso web encajan bien para este tipo de búsquedas. Al menos ayudan a encontrar una dirección.
      Eso sí, las URL que dan casi siempre son alucinaciones.
    • No sé si Google rastrea el aumento repentino de interés en ese artículo por esta publicación de HN, pero al buscar “how does simultaneous multi threading work”, a mí esta entrada del blog me aparece más o menos como el quinto resultado.
      Lo comprobé en una pestaña privada nueva de Firefox en otro dispositivo; no evita por completo el rastreo ni el caché, pero me parece una aproximación bastante razonable.
  • La explicación de que “en un núcleo de CPU con SMT activado, muchos búferes y recursos de ejecución deben ser compartidos por dos procesadores lógicos, así que incluso si solo se está ejecutando un hilo en un núcleo SMT, ese hilo no puede usar esos recursos y el rendimiento potencial se reduce” ya no es cierta hoy en día.
    En modo SMT se reparten el ROB, el ancho de banda de fetch/decode, etc., pero he visto varios núcleos SMT que, cuando no están en SMT, permiten usar todo.

    • Los procesadores Phi de la serie x200 funcionan exactamente así. En modo no SMT, cada hilo obtiene muchos más recursos que en modo SMT de 4 vías.
  • El objetivo principal de SMT es maximizar la utilización del motor de ejecución superescalar.
    Esto me hace preguntarme si la gente piensa que lo superescalar ya no es tan importante como antes.

  • En general es un buen resumen, pero por momentos se sentía un poco mezclado.
    Me gustaría saber más de los trucos del oficio que usan quienes trabajan en esto, aunque sea fuera de lo relacionado con seguridad.

  • A la pobre arquitectura Bulldozer de AMD la criticaron muchísimo por no tener SMT, y ahora todos se están alejando de SMT.
    Claro, sé que Bulldozer tenía problemas mucho más graves además de la falta de SMT. En realidad, era casi la estructura opuesta, donde varios núcleos compartían cosas como la misma ALU. Aun así, si hubieran logrado exprimirle un poco más de rendimiento, quizá habría parecido que se adelantó a algo.

    • La arquitectura PowerXX no se está alejando de SMT.
      Power10 actualmente soporta de forma efectiva SMT8, con 8 hilos por núcleo, y viendo el esfuerzo de años que han dedicado a seguir desarrollando diseños centrados en SMT, no parece probable que lo abandonen.
  • Vale la pena saber que las unidades de cómputo de las GPU también suelen usar una especie de SMT de unas 7 a 10 hebras por CU.
    Este enfoque ayuda a ocultar la latencia.

    • La mayoría de las GPU no usan SMT sino su precursor, el multihilo de grano fino.
      En cada ciclo de reloj, eligen e inician la instrucción de entre varios hilos disponibles que necesite recursos que no estén ocupados. La mayoría de las GPU no inician varias instrucciones por ciclo, aunque varias instrucciones sí pueden seguir avanzando al mismo tiempo una vez iniciadas. Incluso si inician varias instrucciones por ciclo, puede que tengan que pertenecer a clases de instrucciones separadas que usan recursos de ejecución distintos, como instrucciones escalares e instrucciones vectoriales.
      SMT, es decir, multihilo simultáneo, consiste en que en cada ciclo de reloj se inician simultáneamente muchas instrucciones de todos los hilos, y esas instrucciones compiten por las distintas unidades de ejecución de una CPU superescalar para mantener ocupadas tantas como sea posible. Para cada unidad de ejecución en paralelo, como cada uno de los 6 sumadores enteros de una CPU moderna, se decide por separado qué instrucción ejecutar a partir de una cola que contiene instrucciones de todos los hilos simultáneos.