Más allá del htop: E-Cores y P-cores de forma gráfica en Alder Lake i5-12600K, como cambian de core los procesos en linux.

Desde que instalé mi i5-12600, he estado obsesionado con entender cómo gestiona Linux su arquitectura híbrida. En herramientas como htop o btop, vemos porcentajes de uso que se actualizan cada pocos segundos. 

Hoy vamos a bajar al nivel del kernel para ver cómo el scheduler de Linux (el orquestador de procesos) mueve los hilos entre núcleos de alto rendimiento (P-Cores) y de eficiencia (E-Cores).

Como vemos en la imagen ( HTOP )Core 6, 8 y 11 están usando el 100 % del CPUPero, ¿qué pasa realmente en la escala de microsegundos?

Así que hace días estoy trasteando para ver esto más a nivel del kernel, ver cómo se orquesta esto a nivel del scheduler de linux

Cuando vemos htop, vemos carga de CPU y unas barritas que solo nos dicen cuánta carga hay y cuánto CPU estamos usando, esperas y demás, pero a un nivel más bajo, ¿qué sucede? ¿Cada cuánto cambia un proceso de CORE?

Miremos una gráfica que he generado, para entender mejor, básicamente se han capturado los IN y OUT de los procesos, o sea cuando un proceso salta de un nucleo a otro -> https://docs.kernel.org/trace/rv/monitor_sched.html

Como se gestionan cientos de procesos internos, he filtrado la misma traza con algunos procesos para que se vea mejor el salto entre otros cores y no se vean simplemente colores por todo lado:

Filtrados los siguientes procesos en ejecución al generarse la gráfica:

Chrome en ejecución
Stress-ng lanzado con --cpu 2
Xorg ( las X )
macro que gestiona Mate
IPC IO es interno de chrome

Antes que nada:

P-Cores de 0 a 11
E-Cores de 12 a 15

Recordemos: no es el USO de CPU, es el cambio de CPU de un proceso.

Vemos cómo Chrome está "bailando" en los E-cores principalmente, ya que está sin hacer nada. Stress-ng (en verde) es un proceso que consume mucho y, si lo vemos en el top, aparece siempre consumiendo. Se ve cómo el scheduler lo mueve constantemente de core y, al tener 2 hilos, puede estar en 2 cores a la vez, pero con un constante cambio y manteniéndose solo en los P-CORES.

Esta gráfica por sí sola ya deja claro cómo funcionan el Hardware Feedback Interface (HFI) de Intel y el EEVDF scheduler de Linux, trabajando juntos para decidir en qué cores dejar los procesos.

Veamos la misma gráfica añadiendo un backup de borg que estaba excluido de la gráfica inicial:

Borg tiene varias fases: no consume mucho CPU, analiza, deduplica, comprime, etc. Se ve que entra tanto en P-cores como en E-cores, mayormente en P-cores cuando hace el trabajo pesado. El backup no tardó mucho, solo tenía que deduplicar alguna cosilla del día.

Aquí tienes la corrección de este bloque. He pulido la puntuación y las tildes para que la aclaración se entienda perfectamente, manteniendo tu estilo directo:

Los 8 procesos con más "switches"

Ahora, veamos los 8 procesos que más switch han hecho en los 60 segundos en los que capturamos la actividad.

Aclaración: En la siguiente gráfica se han excluido: swapper, kworker, perf, rcu_preempt, migration e idle. Si no se hace este filtro, estos procesos internos lo acaparan todo, ya que se la pasan "bailando" todo el rato.

 

Curiosamente, no vemos a stress-ng en este ranking aunque estuvo consumiendo el 100 % de CPU de 2 cores. Esto es porque no estamos midiendo el USO de CPU, sino las veces que un proceso cambia de core.

Por eso vemos a Xorg primero (el que se encarga de generar la imagen, ratón, teclado y todo lo que se ve en el monitor) y luego a Marco, el gestor de ventanas de Mate.

Son dos de los procesos que siempre van a estar ahí y son los que más "bailan" entre cores, aunque no sean los que más consumen en este caso. Esto nos dice lo siguiente: cuanto más ligero sea el escritorio, mejor. Es lo que más actividad genera en el CPU de lejos, por encima de stress-ng, que estuvo 60 segundos consumiendo el 200 % de CPU pero no bailó tanto como Xorg y Marco.

Mola mucho ver esto, así que vamos a dejar stress-ng de lado y voy a lanzar otros procesos para comparar:

p-cores y e-cores y taskset.

 

No solo quiero ver cómo baila el CPU, sino que taskset me haga caso: si quiero ejecutar un proceso pesado en un E-core, que esto sea posible y no baile ni un milímetro.

Por lo tanto: zstd con 3 hilos, un tar.gz monohilo, taskset con un zip y Handbrake convirtiendo un vídeo. Vamos a ver cuánto "baila" todo esto:

zstd -T3 code/maks.tar -o code/maks.zst &
tar -czf code/maks2.tar.gz maks/ &
taskset -c 15 zip -r code/maks_backup.zip maks/ &
+ Handbrake ( HBN

Voy a capturar datos durante 1 minuto y que Handbrake comience a trabajar a los 20 segundos de la captura; o sea, 20 segundos después de que inicien los compresores. Y ya que estamos, ¿voy a lanzar un stress-ng al final?

Igual me pasé con lo de los 60 segundos:

[ perf record: Woken up 1345 times to write data ]
[ perf record: Captured and wrote 340,251 MB perf.data (2774424 samples) ]

Y aquí la gráfica; me costó entenderlo y llegué a pensar que estaba mal el script con el que generé la gráfica. Lo revisé, miré el fichero de log y lo comprendí todo. La verdad es que "mola mucho" ver todo esto.

Procesos pesados cambiando de core en linux:

Interacciones totales:

ghb              866775
kworker/u64      208646
zstd             179582
tar               63317
mate-terminal     21906
gzip               8642
zip                3700
kworker/6           512
kworker/14          411
kworker/2           407
kworker/u65         380
kworker/0           378
kworker/4           353
kworker/13          337
kworker/8           326

He incluido los kworkers; luego vemos por qué son tan importantes.

He modificado un poco la gráfica y subo dos versiones: una que incluye el kworker (que mete mucho ruido) y otra sin él.

NOTA: la gráfica no se ve muy fina, hay que ajustar el generador, pero es por que en esas lineas hay repartidas más de 1 millón de puntitos 
¿Ven exceso de kworker ? recordemos antiguos POST de este blog, mi equipo tiene  Btrfs sobre LUKS2 y esto añade una capa de complejidad que el scheduler de Linux debe orquestar, ya vimos como bajamos de +200K iops a solo 85k IOPS en otras pruebas. En las gráficas se ve cómo los procesos saltan mucho a los kworkers esperando IO, el disco es bastante rápido pero el kernel está cifrando datos sobre la marcha y gestionando un sistema de archivos Copy-on-Write. Es el precio que pagamos por tener seguridad y snapshots, y aquí pueden ver cómo se traduce en milisegundos de espera en la CPU.

Sin kworker

ZSTD fue el que menos E-cores tocó.

Entendamos algunas cosas y por qué es importante el kworker. Primero veamos qué pasa con zip: lo hemos forzado a trabajar en el core 15 y ahí se quedó, pero tiene 3,700 "cambios" en 60 segundos.

Esto es lo que hace zip:

 kworker/15:3-mm   24085 [015]  9001.700277: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9001.988274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9001.988280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.116274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.116280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.244275: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.244284: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.308280: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.308286: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.436273: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.436280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.596273: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.596280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.656272: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.656334: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.692272: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
 kworker/15:3-mm   24085 [015]  9002.692276: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
             zip   24873 [015]  9002.916274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]

 

Aquí vamos a entender algunas cosas interesantes. Todos los que administramos sistemas siempre nos hemos preguntado qué hace kworker, ya que siempre hay procesos de kworker con un número. Supongo que todos lo hemos buscado en Google; aquí pego una de las primeras respuestas que salen, que me parece la explicación más simple y directa:

https://unix.stackexchange.com/questions/436671/difference-between-kworker-n-and-events-n



Cito "kworker" is a placeholder process for kernel worker threads, which perform most of the actual processing for the kernel, especially in cases where there are interrupts, timers, I/O, etc."

Ayuda en operaciones de disco, entre otras cosas, y el ejemplo de las entradas y salidas de zip en el core 15 es un caso claro.

kworker/15 significa que este kworker es el que trabaja en el core 15 y no sale de ahí (el número es el core; cada núcleo tiene su "ayudante"). zip está procesando (comprimiendo), pero el kworker ayuda con las operaciones de lectura y escritura de disco: (leer) -> comprimir -> (escribir).

Esto aclara algunas dudas: zip trabaja solo en el core 15, pero ha tenido 3.700 salidas/entradas para dar paso al kworker. Podemos intuir que las salidas son para esperar respuesta del disco.

En cambio tar baila por todos los cores: más de 67 000 cambios, tanto de núcleo como de estado:

swapper       0 [014]  8988.887349: sched:sched_switch: swapper/14:0 [120] R ==> tar:24871 [120]
             tar   24871 [014]  8988.887373: sched:sched_switch: tar:24871 [120] S ==> swapper/14:0 [120]
         swapper       0 [008]  8988.888172: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
             tar   24871 [008]  8988.888186: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
         swapper       0 [008]  8988.888836: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
             tar   24871 [008]  8988.888845: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
         swapper       0 [008]  8988.889639: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
             tar   24871 [008]  8988.889651: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
         swapper       0 [008]  8988.890322: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
             tar   24871 [008]  8988.890346: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
         swapper       0 [000]  8988.891058: sched:sched_switch: swapper/0:0 [120] R ==> tar:24871 [120]
             tar   24871 [000]  8988.891071: sched:sched_switch: tar:24871 [120] S ==> swapper/0:0 [120]
         swapper       0 [012]  8988.891880: sched:sched_switch: swapper/12:0 [120] R ==> tar:24871 [120]
             tar   24871 [012]  8988.891905: sched:sched_switch: tar:24871 [120] S ==> swapper/12:0 [120]
         swapper       0 [000]  8988.892682: sched:sched_switch: swapper/0:0 [120] R ==> tar:24871 [120]

Si nos fijamos en la columna 8, el proceso pasa a suspended (suspendido); entra swapper mientras espera y así 67 mil veces, entre cambios de estado y brincos de un core a otro.

El motivo de este cambio de estado tan brutal son las operaciones de I/O. La CPU va muy rápido y el disco va muy lento; el kernel tiene una eficiencia brutal y esos micro y milisegundos de suspended controlan la temperatura y evitan el colapso. O sea, en ese pequeñísimo tiempo en el que tar espera respuesta del disco, el kernel suspende el proceso, parece una tontería, pero ante millones de operaciones, esas pausas y cambios de estado, ahorran energía, evitan calor etc

Para el que lo quiera ver mejor:

  • Columna 3: El número de core.

  • Columna 8: El estado.

    • S (Interruptible sleep): Significa que está esperando un evento.

    • R (Running): El proceso está ejecutándose o listo para ello.

Estos estados son algo básico en Linux; es lo que vemos siempre en top, ps, etc.

Esta entrada explica los estados: https://www.supportpro.com/blog/process-status-ps/

Copio y pego aquí lo básico.

D ­ Uninterruptible sleep (usually IO)­ a blocked state. The process waits for a hardware condition and cannot handle any signal
R ­ Running ­ Process is either running or ready to run
S ­ Interruptible sleep (waiting for an event to complete) ­ a Blocked state of a process and waiting for an event or signal from another process
T ­ Stopped, either by a job control signal or because it is being traced ­ Process is stopped or halted and can be restarted by some other process
X dead (should never be seen)
Z Defunct (“zombie”) process, terminated but not reaped by its parent­ process terminated, but the information is still there in the process table.

Pues eso es la columna 8, constante cambio de R a S, trabajando y esperando datos mayormente del disco.

Luego esta ghb que es Handbrake codificando un pequeño vídeo de 5 minutos:

         swapper       0 [001]  8998.232422: sched:sched_switch: swapper/1:0 [120] R ==> ghb:25086 [120]
         swapper       0 [003]  8998.232425: sched:sched_switch: swapper/3:0 [120] R ==> ghb:25087 [120]
         swapper       0 [008]  8998.232429: sched:sched_switch: swapper/8:0 [120] R ==> ghb:25088 [120]
         swapper       0 [004]  8998.232429: sched:sched_switch: swapper/4:0 [120] R ==> ghb:25089 [120]
             ghb   25002 [012]  8998.232439: sched:sched_switch: ghb:25002 [120] R ==> ghb:25092 [120]
             ghb   25092 [012]  8998.232445: sched:sched_switch: ghb:25092 [120] S ==> ghb:25090 [120]
             ghb   24975 [006]  8998.232965: sched:sched_switch: ghb:24975 [120] S ==> swapper/6:0 [120]
             ghb   24970 [013]  8998.233344: sched:sched_switch: ghb:24970 [120] R ==> kworker/u64:20:24473 [120]
 kworker/u64:20-   24473 [013]  8998.233359: sched:sched_switch: kworker/u64:20:24473 [120] I ==> ghb:24970 [120]
             ghb   24976 [014]  8998.233368: sched:sched_switch: ghb:24976 [120] R ==> Xorg:2594 [120]
            Xorg    2594 [014]  8998.233417: sched:sched_switch: Xorg:2594 [120] S ==> ghb:24976 [120]
             ghb   25089 [004]  8998.233894: sched:sched_switch: ghb:25089 [120] S ==> swapper/4:0 [120]
             ghb   24976 [014]  8998.234110: sched:sched_switch: ghb:24976 [120] S ==> marco:3214 [120]
             ghb   25085 [007]  8998.234328: sched:sched_switch: ghb:25085 [120] S ==> swapper/7:0 [120]
             ghb   24978 [011]  8998.234741: sched:sched_switch: ghb:24978 [120] S ==> swapper/11:0 [120]
             ghb   25090 [012]  8998.235155: sched:sched_switch: ghb:25090 [120] S ==> ghb:25002 [120]
         swapper       0 [014]  8998.235162: sched:sched_switch: swapper/14:0 [120] R ==> ghb:25092 [120]
         swapper       0 [007]  8998.235165: sched:sched_switch: swapper/7:0 [120] R ==> ghb:25093 [120]
             ghb   24970 [013]  8998.235165: sched:sched_switch: ghb:24970 [120] S ==> swapper/13:0 [120]
         swapper       0 [011]  8998.235166: sched:sched_switch: swapper/11:0 [120] R ==> ghb:25095 [120]
         swapper       0 [004]  8998.235170: sched:sched_switch: swapper/4:0 [120] R ==> ghb:25094 [120]
         swapper       0 [013]  8998.235171: sched:sched_switch: swapper/13:0 [120] R ==> ghb:25097 [120]

 

Aquí no vamos a explicar nada ya, creo que esta todo dicho, pero como ha tenido 870 mil saltos de core me entro la curiosidad a ver cuanto salto y en que cores estuvo:

grep "ghb" salida.perf | awk '{print $3}' | tr -d '[]' | sort -n | uniq -c
  90523 001
 100340 002
  72286 003
  94990 004
  78124 005
  98588 006
  82533 007
  95119 008
  79904 009
  94229 010
  76483 011
 122725 012
 110969 013
 100647 014
   2969 015

 

Me resulta llamativo: ha bailado casi por igual en todos los cores. No exactamente igual, pero bailó mucho, menos en el core 15, que tenía a zip ahí agarrado y el kernel ha respetado eso. Como zip no bailaba, el núcleo no quedaba libre y GHB no pudo pillar ese core casi nunca. Supongo que las interacciones que ha tenido son de cuando zip entraba en estado S; o sea, se dormía esos momentos en los que esperaba al disco.

Por último, para que se vean mejor las gráficas, las divido por proceso; así hay menos ruido:

  • Solo ZIP con taskset: (No se ejecuta en ningún core más que el 15, tal como pedimos).

Solo zstd con opción de 3 hilos, generó mucho baile pero acabo en 30 segundos

 

HandBrake multi hilo, casi no toca el core 15 en el que tenemos ZIP, como ya vimos.

Y tar que hace lo que le da la gana, muchas lecturas y escrituras, estaba trabajando en una carpeta con 244 mil archivos cuando se capturo la gráfica.

 

¿Que hizo el resto?

Hemos visto cosas interesantes: con varios procesos que "chupan" CPU y un proceso enganchado al core 15, ¿qué pasó con los demás?

Veamos, durante esos 60 segundos, cuáles fueron los procesos que más veces tuvieron operaciones de switch:

Sí llama la atención AV, es de libav es parte de handbrake, debí incluir ambos en el mismo saco.

Cargas altas de trabajo y los e-cores y p-cores en linux

Muchos puntos y mucha gráfica, pero ¿ves procesos en todos los cores, no?

El asunto aquí es que Linux usa todos los cores, para eso están ahí. No es que los E-cores no ejecuten cargas de trabajo pesadas; los E-cores están ahí para consumir menos electricidad y que tengas que pagar menos a la compañía eléctrica, solo para eso. El sistema prioriza la velocidad: cuando necesitas potencia, consumes, y cuando el sistema puede ejecutar procesos ligeros en E-cores, lo hace ahí para ahorrar energía.

Hagamos un test más con stress solamente y veamos si, durante 60 segundos, un proceso multihilo que ocupe el 100 % de CPU de 3 cores toca algún E-core.

La primera imagen de esta entrada (el htop con stress --cpu 3), desde el lado del kernel se ve así:

643 saltos de un core a otro y alguna otra operación

Toca los e-cores, pero solo al inicio y mucho menos que a los p-cores, concreta mente ha estado en cada core:

~# grep -w "stress" stress.perf | awk '{print $3}' | tr -d '[]' | sort -n | uniq -c
     78 000
     68 001
    122 002
     42 003
    116 004
    102 005
    138 006
    110 007
    152 008
     44 009
    102 010
     86 011
     30 012
     24 013
     30 014
     42 015

 

NOTA: El script que genera las gráficas lo ha generado la IA en Python, ya que mis conocimientos de Python son algo limitados y no había otra forma de generarlos de manera sencilla.

Es muy interesante ver los procesos a este nivel; ya nos queda algo más claro cómo funcionan los E-cores y P-cores, aunque podemos profundizar más aún. Podemos entrar en los ciclos de CPU durante la carga para ver cuánto se usan realmente los E-cores (Atom) o los cores "de toda la vida":

Y no solo eso, podemos hacer otro POST para ver cuándo la CPU espera al disco y ver qué sucede ahí en mayor profundidad.

Todo esto, si bien es interesante o te puede parecer aburrido, es una forma de entender el sistema y analizar mejor por qué puede ir lento; la mayoría de las veces no es el CPU, es la espera del disco, tal como hablamos en aquel otro hilo de webs con WordPress que van lentas por estar esperando al disco siempre...

En fin, eso es todo.

EDITADO --- Añadidas 2 gráficas más con algunas mejoras:

Ejemplo gráfico, XORG vz CC1 

 

He compilado el kernel 6.18.8 y he aprovechado a tomar una muestra mientras compila

Para los curiosos, vemos esto compilando un kernel usando todos los cores:

E-cores y p-cores en linux durante una compilación

 

Arrancado el 6.18.8 en principio algo mejorado con los modulos de intel y demás, veamos esto

He puesto a compilar el kernel especificando -j 4 ( usar solo 4 cores )

Y el resultado es bastante interesante:

Aquí es donde realmente vemos los saltos de CPU que da el compilador. Los puntos de color azul oscuro no tocan los E-cores; dan saltos en los P-cores, pero tampoco demasiados. Hablamos de que se han hecho 2.308 saltos por parte de cc1, una cifra bastante alejada de Xorg, del que ya hemos hablado y que siempre está activado. Este último ha estado en los E-cores la mayor parte del tiempo, sin consumir muchos recursos.

El salto de Xorg a los P-cores se entiende en esos momentos donde hacía falta más "poder", sobre todo abriendo y moviendo ventanas de un lado a otro cuando tuve algo de actividad mirando cosas.

Ante la duda, vemos a cc1 junto con otros procesos que hay abiertos (Xorg, Marco —el gestor de Mate—, Chrome, mate-terminal...), donde se ve más claro aún que los E-cores están trabajando constantemente:

Al final, ha hecho bien Intel en poner 4 Atom en un lugar donde solo habría sitio para un core normal (por el tamaño físico en el CPU), y se ve que hacen bien su trabajo.

Estamos hablando de que un E-core consume unos 5W a máxima potencia y un P-core unos 25W. Y aquí estaban los E-cores en "idle", haciendo nada, pero con más actividad y más procesos que los P-cores que estaban al 100 %.