2012-06-22 16 views
5

Estoy jugando con subprocesos en C++, en particular usándolos para paralelizar una operación de mapa.C + + sobrecarga de subprocesos

Aquí está el código:

#include <thread> 
#include <iostream> 
#include <cstdlib> 
#include <vector> 
#include <math.h> 
#include <stdio.h> 

double multByTwo(double x){ 
    return x*2; 
} 

double doJunk(double x){ 
    return cos(pow(sin(x*2),3)); 
} 

template <typename T> 
void map(T* data, int n, T (*ptr)(T)){ 
    for (int i=0; i<n; i++) 
    data[i] = (*ptr)(data[i]); 
} 

template <typename T> 
void parallelMap(T* data, int n, T (*ptr)(T)){ 
    int NUMCORES = 3; 
    std::vector<std::thread> threads; 
    for (int i=0; i<NUMCORES; i++) 
    threads.push_back(std::thread(&map<T>, data + i*n/NUMCORES, n/NUMCORES, ptr)); 
    for (std::thread& t : threads) 
    t.join(); 
} 

int main() 
{ 
    int n = 1000000000; 
    double* nums = new double[n]; 
    for (int i=0; i<n; i++) 
    nums[i] = i; 

    std::cout<<"go"<<std::endl; 

    clock_t c1 = clock(); 

    struct timespec start, finish; 
    double elapsed; 

    clock_gettime(CLOCK_MONOTONIC, &start); 

    // also try with &doJunk 
    //parallelMap(nums, n, &multByTwo); 
    map(nums, n, &doJunk); 

    std::cout << nums[342] << std::endl; 

    clock_gettime(CLOCK_MONOTONIC, &finish); 

    printf("CPU elapsed time is %f seconds\n", double(clock()-c1)/CLOCKS_PER_SEC); 

    elapsed = (finish.tv_sec - start.tv_sec); 
    elapsed += (finish.tv_nsec - start.tv_nsec)/1000000000.0; 

    printf("Actual elapsed time is %f seconds\n", elapsed); 
} 

Con multByTwo la versión paralela es en realidad un poco más lenta (1,01 frente a 0,95 segundos en tiempo real), y con su doJunk (51 frente a 136 en tiempo real) más rápido. Esto implica para mí que

  1. la paralelización está trabajando, y
  2. hay una muy gran sobrecarga con declarar nuevos temas. ¿Alguna idea de por qué la sobrecarga es tan grande y cómo puedo evitarla?
+2

Tenga en cuenta que esto no es necesariamente específico para * hilos nativos en C++ *, sino la * implementación * y el compilador que utiliza. Como tal, es difícil dar una respuesta definitiva. – zxcdw

+0

¿Con qué hardware está ejecutando este código? Tipo de procesador y número de enchufes? ¿RAM? OS? ¿Versión del compilador? –

Respuesta

7

Solo supongo: lo que probablemente vea es que el código multByTwo es tan rápido que está logrando la saturación de la memoria. El código nunca se ejecutará más rápido, sin importar la cantidad de energía del procesador que arrojes, porque ya va tan rápido como puede obtener los bits desde y hacia la RAM.

+0

Esto parece correcto. El OP tiene un conjunto de datos de 8GB. 8 GB en 1.01 segundos suena perfecto para un procesador Nehalem de gama alta o un procesador de generación Sandy Bridge de gama baja. – Mysticial

0

Engendrar nuevos hilos puede ser una operación costosa dependiendo de la plataforma. La forma más fácil de evitar esta sobrecarga es generar algunos hilos en el lanzamiento del programa y tener algún tipo de cola de trabajos. Creo que std :: async hará esto por ti.

+1

El OP solo está generando una vez, y la tarea es bastante grande 'n = 1000000000'. Entonces, no creo que este sea el caso. – Mysticial

+0

Mi mal.No estoy leyendo lo suficiente :-P –

+0

Creo que el resultado final será el mismo, si el número de subprocesos es menor que el número devuelto por std :: hilo :: hardware_concurrencia() – manasij7479

2

Los subprocesos múltiples solo pueden hacer más trabajo en menos tiempo en una máquina multinúcleo.

En otro caso, solo están tomando turnos en una moda de Round-Robin.

+0

¡NO ESTÉ SEGURO DE QUE ESTA ES UNA DECLARACIÓN CORRECTA! Mira mi respuesta! – trumpetlicks

+0

No estoy hablando del rendimiento 'percibido' y la IU. Estoy hablando de un trabajo real. Si solo hay un procesador, solo se puede ejecutar un hilo a la vez. –

+0

Esto es cierto, pero la forma en que el sistema operativo asigna tiempo a los hilos hace una gran diferencia. Lo he visto en el mundo real con aplicaciones que me obligaron a escribir en la escuela (mucho antes de multi-core) que el rendimiento se incrementó enormemente al enhebrarlas. Mira mi respuesta, explico el efecto round robin, no uso ese término, ¡pero se explica por qué a una aplicación de subprocesos múltiples se le asignan más tiempos de tiempo de procesador! – trumpetlicks

3

No especificó el hardware que prueba su programa ni la versión del compilador y el sistema operativo. Probé tu código en nuestros sistemas Intel Xeon de cuatro sockets en Scientific Linux de 64 bits con g++ 4.7 compilados de la fuente.

En primer lugar en un sistema más antiguo Xeon X7350 Tengo los siguientes tiempos:

multByTwo con map

CPU elapsed time is 6.690000 seconds 
Actual elapsed time is 6.691940 seconds 

multByTwo con parallelMap en 3 núcleos

CPU elapsed time is 7.330000 seconds 
Actual elapsed time is 2.480294 seconds 

El aumento de velocidad en paralelo es 2.7x.

doJunk con map

CPU elapsed time is 209.250000 seconds 
Actual elapsed time is 209.289025 seconds 

doJunk con parallelMap en 3 núcleos

CPU elapsed time is 220.770000 seconds 
Actual elapsed time is 73.900960 seconds 

El aumento de velocidad paralelo es 2.83x.

Tenga en cuenta que X7350 es de la bastante antigua familia "Tigerton" pre-Nehalem con bus FSB y un controlador de memoria compartida ubicado en el puente norte. Este es un sistema SMP puro sin efectos NUMA.

Luego ejecuto su código en un Intel X7550 de cuatro sockets. Estos son Xeons Nehalem ("Beckton") con controlador de memoria integrado en la CPU y, por lo tanto, un sistema NUMA de 4 nodos. Los subprocesos que se ejecutan en un socket y que acceden a la memoria ubicada en otro socket se ejecutarán un poco más despacio. Lo mismo también es cierto para un proceso en serie que puede ser migrado a otro socket por alguna estúpida decisión del planificador. La unión en un sistema de este tipo es muy importante, ya que puede ver desde los tiempos:

multByTwo con map

CPU elapsed time is 4.270000 seconds 
Actual elapsed time is 4.264875 seconds 

multByTwo con map con destino al nodo NUMA 0

CPU elapsed time is 4.160000 seconds 
Actual elapsed time is 4.160180 seconds 

multByTwo con map con destino a NUMA nodo 0 y socket de CPU 1

CPU elapsed time is 5.910000 seconds 
Actual elapsed time is 5.912319 seconds 

mutlByTwo con parallelMap en 3 núcleos

CPU elapsed time is 7.530000 seconds 
Actual elapsed time is 3.696616 seconds 

aumento de velocidad en paralelo sólo 1.13x (en relación con la ejecución en serie más rápida nodo determinada). Ahora, con la unión:

multByTwo con parallelMap en 3 núcleos unidos a nodo NUMA 0

CPU elapsed time is 4.630000 seconds 
Actual elapsed time is 1.548102 seconds 

aceleración paralelo es 2.69x - tanto como para la CPU Tigerton.

multByTwo con parallelMap en 3 núcleos unidos a nodo NUMA 0 y zócalo de la CPU 1

CPU elapsed time is 5.190000 seconds 
Actual elapsed time is 1.760623 seconds 

aceleración paralelo es 2.36x - 88% del caso anterior.

(I era demasiado impaciente para esperar a que el código doJunk para terminar en los Nehalems relativamente lentas, pero que se puede esperar algo mejor rendimiento al igual que en el caso Tigerton)

Hay una advertencia en la NUMA vinculante sin embargo. Si fuerza, p. enlazar al nodo 0 de NUMA con numactl --cpubind=0 --membind=0 ./program esto limitará la asignación de memoria a este nodo solamente y en su sistema particular la memoria conectada a la CPU 0 puede no ser suficiente y es muy probable que ocurra una falla en tiempo de ejecución.

Como puede ver, existen otros factores, además de la sobrecarga de la creación de subprocesos, que pueden influir significativamente en el tiempo de ejecución del código. También en sistemas muy rápidos, la sobrecarga puede ser demasiado alta en comparación con el trabajo computacional realizado por cada hilo. Es por eso que cuando se hacen preguntas sobre el rendimiento paralelo, uno siempre debe incluir la mayor cantidad de detalles posible sobre el hardware y el entorno utilizado para medir el rendimiento.

+0

¡Gracias por la respuesta detallada! – andyInCambridge