¿Cuál es la mejor manera de determinar el número de subprocesos que se activarán en una máquina con n núcleos?(C++)
multithreading multicore (6)
El número óptimo de núcleos (subprocesos) probablemente se determinará cuando logre la saturación del sistema de memoria (cachés y RAM). Otro factor que podría entrar en juego es el bloqueo entre núcleos (bloqueo de un área de memoria a la que otros núcleos podrían querer acceder, actualizarlo y luego desbloquearlo) y su eficiencia (la duración del bloqueo y su frecuencia). está bloqueado / desbloqueado).
Un solo núcleo que ejecuta un software genérico cuyo código y datos no están optimizados para múltiples núcleos se acercará a saturar la memoria por sí solo. Agregar más núcleos, en tal escenario, resultará en una aplicación más lenta.
Entonces, a menos que su código ahorre mucho en los accesos a la memoria, supongo que la respuesta a su pregunta es una (1).
Tengo un vector<int>
con 10,000,000 (10 millones) de elementos, y mi estación de trabajo tiene cuatro núcleos. Hay una función, llamada ThrFunc
, que opera en un entero. Suponga que el tiempo de ejecución de ThrFunc
para cada entero en el vector<int>
es aproximadamente el mismo.
¿Cómo debo determinar el número óptimo de hilos para disparar? ¿Es la respuesta tan simple como el número de elementos dividido por el número de núcleos? ¿O hay una computación más sutil?
Edición para proporcionar información adicional
- No hay necesidad de bloqueo; Cada invocación de función solo necesita acceso de solo lectura
El número óptimo de subprocesos debe ser igual al número de núcleos, en cuyo caso la capacidad de cálculo de cada núcleo se utilizará completamente, si el cálculo de cada elemento es independiente.
Es probable que la cantidad óptima de hilos sea la cantidad de núcleos en su máquina o la cantidad de núcleos por dos.
En términos más abstractos, desea el mayor rendimiento posible. Obtener el rendimiento más alto requiere la menor cantidad de puntos de contención entre los hilos (ya que el problema original es trivialmente paralelizable). Es probable que el número de puntos de contención sea el número de subprocesos que comparten un núcleo o el doble, ya que un núcleo puede ejecutar uno o dos subprocesos lógicos (dos con hyperthreading).
Si su carga de trabajo utiliza un recurso del cual tiene menos de cuatro disponibles (¿ALU en Bulldozer? ¿Acceso al disco duro?), La cantidad de subprocesos que debe crear estará limitada por eso.
La mejor manera de encontrar la respuesta correcta es, con todas las preguntas de hardware, probar y descubrir.
Estoy de acuerdo con los comentarios anteriores. Debe ejecutar pruebas para determinar qué número produce el mejor rendimiento. Sin embargo, esto solo proporcionará el mejor rendimiento para el sistema en particular que está optimizando. En la mayoría de los escenarios, su programa se ejecutará en las máquinas de otras personas, en cuya arquitectura no debe hacer demasiadas suposiciones.
Una buena manera de determinar numéricamente el número de subprocesos que se iniciaría sería usar
std::thread::hardware_concurrency()
Esto es parte de C ++ 11 y debe proporcionar el número de núcleos lógicos en el sistema actual. Los núcleos lógicos significan el número físico de núcleos, en caso de que el procesador no admita hilos de hardware (es decir, HyperThreading), o el número de hilos de hardware.
También hay una función Boost que hace lo mismo, consulte Búsqueda programática del número de núcleos en una máquina .
Suponiendo que ThrFunc
está ThrFunc
CPU, entonces probablemente desee un hilo por núcleo y divida los elementos entre ellos.
Si hay un elemento de E / S en la función, la respuesta es más complicada, ya que puede tener uno o más subprocesos por núcleo en espera de E / S mientras se está ejecutando otro. Hacer algunas pruebas y ver qué pasa.
La respuesta de Borealid incluye pruebas y descubrimientos , lo cual es imposible de superar según los consejos.
Pero tal vez haya más pruebas de lo que podría pensar: desea que sus hilos eviten la contención de datos siempre que sea posible. Si los datos son completamente de solo lectura, entonces puede ver el mejor rendimiento si sus subprocesos acceden a datos "similares", asegurándose de recorrer los datos en pequeños bloques a la vez, de modo que cada subproceso accede a los datos desde las mismas páginas. una y otra vez Si los datos son completamente de solo lectura, entonces no hay problema si cada núcleo obtiene su propia copia de las líneas de caché. (Aunque esto podría no hacer el mayor uso de la memoria caché de cada núcleo).
Si los datos se modifican de alguna manera, entonces verás mejoras significativas en el rendimiento si mantienes los hilos alejados unos de otros, por mucho. La mayoría de los cachés almacenan datos a lo largo de las líneas de caché , y usted desea desesperadamente evitar que cada línea de caché rebote entre las CPU para un buen rendimiento. En ese caso, es posible que desee mantener los distintos subprocesos ejecutándose en datos que en realidad están muy separados para evitar que se encuentren entre sí.
Entonces, si está actualizando los datos mientras trabaja en ellos, recomendaría tener N o 2 * N hilos de ejecución (para N núcleos), comenzándolos con SIZE / N * M como punto de inicio, para los hilos de 0 a M. (0, 1000, 2000, 3000, para cuatro subprocesos y 4000 objetos de datos). Esto le dará la mejor oportunidad de alimentar diferentes líneas de caché a cada núcleo y permitir que las actualizaciones continúen sin rebotar las líneas de caché:
+--------------+---------------+--------------+---------------+--- ...
| first thread | second thread | third thread | fourth thread | first ...
+--------------+---------------+--------------+---------------+--- ...
Si no está actualizando los datos mientras está trabajando en ellos, es posible que desee iniciar N o 2 * N subprocesos de ejecución (para N núcleos), comenzándolos con 0, 1, 2, 3, etc. y moviendo cada uno reenviar por N o 2 * N elementos con cada iteración. Esto permitirá que el sistema de caché obtenga cada página de la memoria una vez, rellene los cachés de la CPU con datos casi idénticos y, con suerte, mantenga cada núcleo lleno de datos nuevos.
+-----------------------------------------------------+
| 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 ... |
+-----------------------------------------------------+
También recomiendo usar sched_setaffinity(2)
directamente en tu código para forzar a los diferentes hilos a sus propios procesadores. En mi experiencia, Linux apunta a mantener cada subproceso en su procesador original tanto que no migrará las tareas a otros núcleos que, de lo contrario, estarán inactivos.