¿Cómo se comportan los tiempos de ejecución al incrementar el número de hilos?

¿Cómo se comportan los tiempos de ejecución al incrementar el número de hilos?


Introducción

En esta entrada se analiza el rendimiento de un programa de procesamiento de imágenes BMP de 24 bits. El objetivo del programa es incrementar el brillo de una imagen utilizando una rutina SIMD en ensamblador y dividiendo el trabajo entre varios hilos.


La pregunta que se busca responder es la siguiente:


¿Cómo se van a comportar los tiempos de ejecución al incrementar el número de hilos con un mismo tamaño de imagen a procesar?

Esta pregunta es importante porque permite observar si el uso de más hilos realmente mejora el rendimiento del procesamiento de imágenes, y también permite identificar si existe un punto donde agregar más hilos deja de ser útil.


Hipótesis iniciales

Hipótesis 1:

Se debería reducir el tiempo de ejecución, ya que con un mayor número de hilos se puede dividir la tarea en más partes y así ejecutar el código al mismo tiempo, consiguiendo así un menor tiempo de ejecución.


Hipótesis 2:

El tiempo debería bajar al usar más hilos, pero no siempre va a seguir bajando. Llega un punto en el que agregar más hilos ya no ayuda mucho, sobre todo si se usan más hilos que núcleos físicos tiene el procesador. En ese caso, el sistema empieza a gastar más tiempo organizando, repartiendo y sincronizando el trabajo que procesando la imagen, por eso el rendimiento puede quedarse igual o incluso empeorar.


Metodología y diseño de pruebas


Para comprobar las hipótesis se modificó el programa original para ejecutar automáticamente varias pruebas con diferentes números de hilos. En todas las pruebas se utilizó la misma imagen, con el fin de mantener fijo el tamaño de entrada y analizar únicamente el efecto del número de hilos.

Se realizaron 100 ejecuciones por cada configuración de hilos. En cada ejecución se midió solamente el tiempo de procesamiento del brillo, sin incluir el tiempo de carga ni el tiempo de guardado de la imagen. Esto permite que los resultados representen de forma más directa el costo del procesamiento paralelo.


Las configuraciones evaluadas fueron:


1, 2, 4, 6, 8, 10, 12, 16, 20 y 32 hilos.


El programa generó un archivo llamado tiempos.csv, donde se guardaron los datos crudos de cada ejecución. Las columnas del archivo fueron:

imagen, ancho, alto, bits_por_pixel, bytes_imagen, hilos, ejecucion, tiempo_ms


Especificaciones de hardware


Las pruebas se realizaron en el siguiente equipo:
  • CPU: 12th Gen Intel(R) Core(TM) i7-12650H
  • Núcleos físicos: 10
  • Procesadores lógicos: 16
  • Frecuencia máxima reportada: 2.30 GHz
  • Caché L2: 9728 KB
  • Caché L3: 24576 KB
  • Memoria RAM: 16 GB
  • Equipo: THUNDEROBOT 911MT


Entorno de software
  • Sistema operativo: Microsoft Windows 11 Pro de 64 bits
  • Entorno: Windows nativo, no virtualizado
  • IDE: Microsoft Visual Studio 2022
  • Configuración de compilación: Release x64


No se utilizó máquina virtual. Las pruebas fueron ejecutadas directamente sobre Windows 11 nativo, por lo que no hubo asignación de vCPUs ni RAM virtualizada.


Set de imágenes de prueba


Para esta pregunta se utilizó una sola imagen, ya que el objetivo era analizar el comportamiento del tiempo al variar el número de hilos, manteniendo constante el tamaño de la imagen.


  • Imagen utilizada: nature2.bmp
  • Dimensiones: 3100 x 2325 pixeles
  • Formato: BMP de 24 bits
  • Tamaño de datos de imagen: 21622500 bytes


Implementación del código


El programa utiliza una clase BMP para cargar la imagen y acceder directamente al arreglo de bytes que contiene los pixeles. El incremento de brillo se realiza mediante una rutina SIMD en ensamblador x64 llamada brightness_xmmw.


Cada hilo recibe una parte del arreglo de bytes de la imagen y ejecuta la rutina SIMD sobre su segmento correspondiente. El programa espera a que todos los hilos terminen antes de registrar el tiempo final.


Fragmento representativo de las configuraciones de hilos utilizadas:


int threadCounts[] = { 1, 2, 4, 6, 8, 10, 12, 16, 20, 32 };
Fragmento representativo de la medición:
QueryPerformanceCounter(&start);


Creación de hilos y procesamiento SIMD 

WaitForMultipleObjects((DWORD)threadCount, threads.data(), TRUE, INFINITE);
QueryPerformanceCounter(&end);


El tiempo medido corresponde únicamente al procesamiento de la imagen, no a la lectura ni escritura del archivo BMP.


Resultados


Después de ejecutar 100 pruebas por cada configuración de hilos, se obtuvo la siguiente tabla resumen:


Hilos | Promedio ms | Mínimo ms | Máximo ms | SpeedUp

1     | 3.208688    | 2.436500  | 9.054000  | 1.0000

2     | 2.425915    | 1.845600  | 4.832300  | 1.3227

4     | 2.342777    | 1.782100  | 5.190000  | 1.3696

6     | 2.357825    | 1.853500  | 3.106200  | 1.3609

8     | 2.156250    | 1.708600  | 3.118800  | 1.4881

10    | 2.114847    | 1.718100  | 3.257100  | 1.5172

12    | 2.179509    | 1.655400  | 3.725200  | 1.4722

16    | 2.147353    | 1.716600  | 3.132200  | 1.4943

20    | 2.130497    | 1.762700  | 2.862600  | 1.5061

32    | 2.570150    | 2.020100  | 3.798300  | 1.2484


El SpeedUp fue calculado tomando como referencia el tiempo promedio con 1 hilo. La fórmula utilizada fue:
SpeedUp = Tiempo promedio con 1 hilo / Tiempo promedio con N hilos


Descripción de la gráfica:

La gráfica muestra el tiempo promedio de procesamiento para cada configuración de hilos. Se observa que el tiempo disminuye al pasar de 1 hilo a configuraciones con más hilos. Sin embargo, la disminución no es lineal. A partir de cierto punto, los tiempos se mantienen muy cercanos, y con 32 hilos el tiempo promedio vuelve a aumentar.




Descripción de la gráfica:

La gráfica de SpeedUp muestra la mejora obtenida respecto a la ejecución con 1 hilo. El SpeedUp aumenta al utilizar más hilos, llegando a valores cercanos a 1.5. Sin embargo, no crece proporcionalmente al número de hilos. En la configuración de 32 hilos el SpeedUp disminuye, lo que indica una pérdida de eficiencia al utilizar demasiados hilos.


Descripción de la gráfica:


El diagrama de cajas y bigotes representa la distribución de las 100 ejecuciones realizadas para cada configuración de hilos. Esta gráfica permite observar la variabilidad de los tiempos y detectar valores atípicos. En general, las configuraciones con más hilos presentan tiempos menores que la ejecución con 1 hilo, pero también se observa que el uso excesivo de hilos puede aumentar la dispersión o afectar el rendimiento.


Análisis de resultados

Los resultados muestran que el uso de múltiples hilos sí mejora el tiempo de procesamiento en comparación con la ejecución de un solo hilo. Con 1 hilo, el tiempo promedio fue de 3.208688 ms. Al utilizar 10 hilos, el tiempo promedio bajó a 2.114847 ms, que fue uno de los mejores resultados obtenidos.

Esto confirma que dividir la imagen en partes y procesarlas de forma paralela permite reducir el tiempo de ejecución. Además, el uso de SIMD ayuda a que cada hilo procese varios bytes al mismo tiempo, aprovechando mejor las capacidades del procesador.

Sin embargo, los datos también muestran que la mejora no continúa de manera indefinida. Al pasar de 10 a 12, 16 o 20 hilos, los tiempos se mantienen muy cercanos. Esto indica que después de cierto punto agregar más hilos ya no produce una mejora importante.

El caso más claro se observa con 32 hilos. Aunque se utilizan más hilos, el tiempo promedio aumenta a 2.570150 ms y el SpeedUp baja a 1.2484. Esto demuestra que usar más hilos no siempre significa obtener mejor rendimiento.


Una explicación técnica de este comportamiento es que el procesador utilizado tiene 10 núcleos físicos y 16 procesadores lógicos. Cuando se usan más hilos que los recursos lógicos disponibles, el sistema operativo debe repartir el tiempo de CPU entre más tareas. Esto puede provocar mayor sobrecarga por planificación de hilos, cambios de contexto y administración del trabajo paralelo.


También debe considerarse que el procesamiento medido es muy rápido, con tiempos de apenas unos milisegundos. En tareas tan pequeñas, el costo de crear, administrar y sincronizar hilos puede volverse significativo en comparación con el tiempo real de procesamiento.


Confrontación de hipótesis


Hipótesis 1:

La primera hipótesis se corrobora parcialmente. Los datos muestran que al aumentar el número de hilos el tiempo de ejecución disminuye en comparación con la ejecución de 1 hilo. Esto se puede observar al comparar el promedio de 3.208688 ms con 1 hilo contra los tiempos cercanos a 2.1 ms en configuraciones como 8, 10, 16 y 20 hilos.

Sin embargo, esta hipótesis no explica completamente el comportamiento observado, porque el tiempo no sigue bajando de manera constante conforme se agregan más hilos.

Hipótesis 2:

La segunda hipótesis se corrobora con mayor fuerza. Los resultados muestran que el tiempo baja al usar más hilos, pero llega un punto donde agregar más hilos ya no mejora de forma significativa el rendimiento. Incluso con 32 hilos el tiempo promedio empeora respecto a configuraciones menores.

Esto coincide con la idea de que, al superar cierto número de hilos, el sistema comienza a gastar más tiempo administrando, repartiendo y sincronizando el trabajo. Por lo tanto, el rendimiento puede estancarse o empeorar.


Discusión

El comportamiento observado es consistente con los principios del paralelismo. Al dividir una tarea entre varios hilos, el trabajo puede realizarse en menos tiempo siempre que existan suficientes recursos físicos para ejecutarlos de manera eficiente. No obstante, el paralelismo tiene límites.


En este caso, el procesador cuenta con 10 núcleos físicos y 16 procesadores lógicos. Por eso, las mejoras más importantes se observan al pasar de 1 hilo a configuraciones cercanas al número de núcleos disponibles. Después de ese punto, la mejora se vuelve menor porque los recursos del procesador ya están siendo utilizados de forma considerable.


Además, el sistema operativo debe planificar la ejecución de los hilos. Cuando se crean demasiados hilos, especialmente más que los procesadores lógicos disponibles, aumenta la sobrecarga por planificación y cambios de contexto. Esta sobrecarga puede reducir la ganancia obtenida por paralelismo.

La rutina SIMD también influye en los resultados, ya que permite procesar varios bytes de la imagen en una sola instrucción. Esto hace que el procesamiento individual de cada bloque sea muy rápido. Por esa razón, al usar demasiados hilos, el costo de administrarlos puede ser comparable o incluso mayor que el beneficio obtenido.


Conclusión

Con base en los resultados obtenidos, se concluye que la hipótesis más acertada es la segunda.
El tiempo de ejecución sí disminuye al incrementar el número de hilos, pero no lo hace de manera indefinida. 

La mejora más clara ocurre al pasar de 1 hilo a configuraciones con varios hilos, pero después los tiempos se estabilizan. Cuando se utilizan demasiados hilos, como en el caso de 32 hilos, el rendimiento empeora.


Por lo tanto, para esta imagen y este equipo, el mejor comportamiento se encuentra alrededor de configuraciones cercanas a los núcleos físicos y procesadores lógicos disponibles. 

El experimento demuestra que el paralelismo mejora el rendimiento, pero también que existe un límite práctico impuesto por la arquitectura del procesador y por la sobrecarga del sistema operativo.


Usar más hilos puede reducir el tiempo de ejecución, pero usar demasiados hilos no garantiza un mejor rendimiento.


Link del repositorio y los archivos exel con graficos hechos y resultados.


TheDaniel27/BmpBoostSIMD


Comentarios

Entradas más populares de este blog

¿Cómo se van a comportar los tiempos de ejecución si se mantiene el código, pero se varía el tamaño de la imagen a procesar, en cada configuración? Y ¿existe una variación y si existe cómo se podría explicar tal variación observada?

¿Qué código reportará mejor Speedup, el generado por IA en un lenguaje de alto nivel como Python o el de la versión C/C++?