2009-08-20 20 views
6

Tengo una clase de agrupación de subprocesos personalizada, que crea algunos subprocesos que cada uno espera en su propio evento (señal). Cuando se agrega un nuevo trabajo al grupo de subprocesos, se activa el primer subproceso libre para que ejecute el trabajo.Overhead debido al uso de eventos

El problema es el siguiente: Tengo alrededor de 1000 loops de cada uno alrededor de 10'000 iteraciones. Estos bucles se deben ejecutar secuencialmente, pero tengo 4 CPU disponibles. Lo que intento hacer es dividir los 10'000 bucles de iteración en 4 bucles de 2'500 iteraciones, es decir, uno por hilo. Pero tengo que esperar a que los 4 bucles pequeños terminen antes de pasar a la siguiente iteración "grande". Esto significa que no puedo agrupar los trabajos.

Mi problema es que usar el grupo de subprocesos y 4 subprocesos es mucho más lento que hacer los trabajos secuencialmente (tener un ciclo ejecutado por un subproceso separado es mucho más lento que ejecutarlo directamente en el subproceso principal secuencialmente).

Estoy en Windows, entonces creo eventos con CreateEvent() y luego espero en uno de ellos usando WaitForMultipleObjects(2, handles, false, INFINITE) hasta que el hilo principal llame al SetEvent().

Parece que todo este evento (¡junto con la sincronización entre los subprocesos usando secciones críticas) es bastante caro!

Mi pregunta es: ¿es normal que usar eventos tome "mucho" tiempo? Si es así, ¿hay otro mecanismo que pueda usar y que sea menos costoso en el tiempo?

Aquí hay un código para ilustrar (algunas partes relevantes copiados de mi clase de grupo de subprocesos):

// thread function 
unsigned __stdcall ThreadPool::threadFunction(void* params) { 
    // some housekeeping 
    HANDLE signals[2]; 
    signals[0] = waitSignal; 
    signals[1] = endSignal; 

    do { 
     // wait for one of the signals 
     waitResult = WaitForMultipleObjects(2, signals, false, INFINITE); 

     // try to get the next job parameters; 
     if (tp->getNextJob(threadId, data)) { 
      // execute job 
      void* output = jobFunc(data.params); 

      // tell thread pool that we're done and collect output 
      tp->collectOutput(data.ID, output); 
     } 

     tp->threadDone(threadId); 
    } 
    while (waitResult - WAIT_OBJECT_0 == 0); 

    // if we reach this point, endSignal was sent, so we are done ! 

    return 0; 
} 

// create all threads 
for (int i = 0; i < nbThreads; ++i) { 
    threadData data; 
    unsigned int threadId = 0; 
    char eventName[20]; 

    sprintf_s(eventName, 20, "WaitSignal_%d", i); 

    data.handle = (HANDLE) _beginthreadex(NULL, 0, ThreadPool::threadFunction, 
     this, CREATE_SUSPENDED, &threadId); 
    data.threadId = threadId; 
    data.busy = false; 
    data.waitSignal = CreateEvent(NULL, true, false, eventName); 

    this->threads[threadId] = data; 

    // start thread 
    ResumeThread(data.handle); 
} 

// add job 
void ThreadPool::addJob(int jobId, void* params) { 
    // housekeeping 
    EnterCriticalSection(&(this->mutex)); 

    // first, insert parameters in the list 
    this->jobs.push_back(job); 

    // then, find the first free thread and wake it 
    for (it = this->threads.begin(); it != this->threads.end(); ++it) { 
     thread = (threadData) it->second; 

     if (!thread.busy) { 
      this->threads[thread.threadId].busy = true; 

      ++(this->nbActiveThreads); 

      // wake thread such that it gets the next params and runs them 
      SetEvent(thread.waitSignal); 
      break; 
     } 
    } 

    LeaveCriticalSection(&(this->mutex)); 
} 
+0

edición para precisar su pregunta ... – neuro

Respuesta

1

Si solo está paralelizando bucles y está usando vs 2008, le sugiero que consulte OpenMP. Si usa Visual Studio 2010 beta 1, le sugiero que consulte la parallel pattern library, particularmente la "parallel for"/"parallel for each" apis o la clase "task group, ya que es probable que hagan lo que están intentando hacer, solo que con menos código.

En cuanto a su pregunta sobre el rendimiento, aquí realmente depende. Tendrá que ver cuánto trabajo está programando durante sus iteraciones y cuáles son los costos. WaitForMultipleObjects puede ser bastante caro si se golpea mucho y su trabajo es pequeño, por lo que sugiero usar una implementación ya construida. También debe asegurarse de que no se está ejecutando en modo de depuración, bajo un depurador y que las tareas en sí mismas no están bloqueadas en un bloqueo, E/S o asignación de memoria, y que no está teniendo acceso a la compartición falsa. Cada uno de estos tiene el potencial de destruir la escalabilidad.

Sugiero mirar esto en un generador de perfiles como xperf el profiler f1 en Visual Studio 2010 beta 1 (tiene 2 nuevos modos de concurrencia que ayudan a ver la contención) o la vtune de Intel.

También podría compartir el código que está ejecutando en las tareas, para que la gente tenga una mejor idea de lo que está haciendo, porque la respuesta que siempre obtengo con los problemas de rendimiento es primero "depende" y segundo , "lo has perfilado".

buena suerte

-Rick

+0

Gracias por su respuesta. ¡Aceptaré el suyo ya que proporciona enlaces útiles y sugiero el uso de OpenMP! – Wookai

1

El cambio de contexto entre hilos puede ser costoso también. En algunos casos, es interesante desarrollar un marco que pueda usar para procesar sus trabajos secuencialmente con un hilo o con múltiples hilos. De esta manera puedes tener lo mejor de los dos mundos.

Por cierto, ¿cuál es su pregunta exactamente? Voy a ser capaz de responder con mayor precisión con una pregunta más precisa :)

EDIT:

La parte acontecimientos puede consumir más de su procesamiento en algunos casos, pero no debería ser tan caro, a menos que su procesamiento es realmente rápido de lograr. En este caso, el cambio entre thredas también es costoso, de ahí que mi respuesta sea la primera parte de hacer las cosas secuencialmente ...

Debe buscar los cuellos de botella de sincronización entre subprocesos. Puede rastrear hilos tiempos de espera para empezar ...

EDIT: Después de más pistas ...

Si supongo correctamente, el problema es utilizar de manera eficiente todos sus núcleos de ordenador/procesador a parralellize algún procesamiento essencialy secuencial.

Supongamos que tiene 4 núcleos y 10000 bucles para calcular como en su ejemplo (en un comentario). Dijiste que necesitas esperar a que terminen los 4 hilos antes de continuar. Entonces puedes simplificar tu proceso de sincronización. Solo necesita dar los cuatro hilos enésimo, nth + 1, nth + 2, nth + 3 loops, espere a que se completen los cuatro hilos y luego continúe. Debe usar un punto de encuentro o barrera (un mecanismo de sincronización que espera que n hilos se completen). Boost tiene tal mecanismo. Puede mirar la implementación de Windows para mayor eficiencia. Su grupo de subprocesos no es realmente adecuado para la tarea. La búsqueda de un hilo disponible en una sección crítica es lo que está matando su tiempo de CPU. No es la parte del evento.

+0

Mmmh, creo que mi pregunta es sobre el costo de usar eventos (¿son realmente caros o estoy haciendo las cosas mal?). – Wookai

+0

Sí, edite su pregunta, será mejor ... – neuro

+1

El enfoque de neuro es probablemente su mejor opción. Su otra opción es rediseñar sus bucles para que ya no confíen el uno en el otro, si puede. Es posible que tengas que pagar una penalización de perf, pero está bien: el código que es x2 más lento pero se amplía linealmente con el número de subprocesos de hardware gana en general, ¿verdad? –

1

No debería ser tan caro, pero si su trabajo lleva muy poco tiempo, la sobrecarga de los hilos y los objetos de sincronización se volverán importantes. Los grupos de subprocesos como este funcionan mucho mejor para trabajos de procesamiento más largos o para aquellos que usan mucho IO en lugar de recursos de CPU. Si está vinculado a la CPU al procesar un trabajo, asegúrese de tener solo 1 hilo por CPU.

Puede haber otros problemas, ¿cómo getNextJob obtiene sus datos para procesar? Si hay una gran cantidad de copia de datos, entonces ha aumentado significativamente sus gastos generales nuevamente.

Lo optimizaría dejando que cada hilo siga retirando trabajos de la cola hasta que la cola esté vacía. de esta forma, puede pasar cien trabajos al grupo de subprocesos y los objetos de sincronización se usarán solo una vez para iniciar el hilo. También almacenaba los trabajos en una cola y les pasaba un puntero, referencia o iterador al hilo en lugar de copiar los datos.

+0

Tuve la misma idea de optimización que tú, es decir, dejar que los hilos extraigan trabajos sin pasar por WaitForMultipleObjects(), pero en mi caso tengo muy pocos trabajos por hilo, por lo que esto no cambiará demasiado. – Wookai

+0

Pensé que tenías 2500 por hilo? No importa, la alternativa es verificar OpenMP, que puede ser más rápido y definitivamente más fácil de implementar. (es decir, simplemente pones un pragma antes del ciclo for y lo dejas administrar todo por ti). – gbjbaanb

3

Sí, WaitForMultipleObjects es bastante caro. Si sus trabajos son pequeños, la sobrecarga de sincronización comenzará a abrumar el costo de hacer realmente el trabajo, como está viendo.

Una forma de corregir esto es agrupar varios trabajos en uno: si obtiene un trabajo "pequeño" (sin importar cómo lo evalúe), guárdelo en algún lugar hasta que tenga suficientes trabajos pequeños para realizar un trabajo de un tamaño razonable. Luego, envíelos a un hilo de trabajo para su procesamiento.

Como alternativa, en lugar de utilizar la señalización, puede utilizar una cola de escritor único de varios lectores para almacenar sus trabajos. En este modelo, cada subproceso de trabajo intenta tomar trabajos de la cola. Cuando encuentra uno, hace el trabajo; si no lo hace, duerme durante un corto período, luego se despierta e intenta nuevamente. Esto reducirá la sobrecarga por tarea, pero tus subprocesos tomarán CPU incluso cuando no hay trabajo por hacer. Todo depende de la naturaleza exacta del problema.

+0

El problema es el siguiente: tengo alrededor de 1000 loops de cada uno alrededor de 10'000 iteraciones. Estos bucles se deben ejecutar secuencialmente, pero tengo 4 CPU disponibles. Lo que intento hacer es dividir los 10'000 bucles de iteración en 4 bucles de 2'500 iteraciones, es decir, uno por hilo. Pero tengo que esperar a que los 4 bucles pequeños terminen antes de pasar a la siguiente iteración "grande". Esto significa que no puedo agrupar los trabajos. – Wookai

+0

Ponlo en la pregunta;) – neuro

+0

Ese es el verdadero problema ... Ver mi edición en mi respuesta por mis 2 centavos ... – neuro

3

Esto me parece un patrón de consumidor productor, que se puede implementar con dos semáforos, uno que guarda el desbordamiento de la cola, el otro la cola vacía.

Puede encontrar algunos detalles here.

+0

¿Son los semáforos menos costosos que los eventos? – Wookai

+0

¿Qué significa "caro"? En términos de recursos? En términos de tiempo del núcleo gastado para bloquear/desbloquear? –

+0

No creo que haya una diferencia. De todos modos, una diferencia que se puede ver. Siempre puedes medir con un generador de perfiles. –

2

Tenga cuidado, todavía está solicitando un nuevo trabajo después de que se emite endSignal.

for(;;) { 
    // wait for one of the signals 
    waitResult = WaitForMultipleObjects(2, signals, false, INFINITE); 
    if(waitResult - WAIT_OBJECT_0 != 0) 
     return; 
    //.... 
} 
+0

Gracias por señalar eso. No es un problema porque se llama al endSignal cuando la lista de trabajos está vacía, por lo que no obtendrá ningún trabajo y finalizará correctamente. ¡Pero tienes toda la razón! – Wookai

1

Parece que todo esto evento (junto con la sincronización entre las roscas usando secciones críticas) es bastante caro!

"Expensive" es un término relativo. ¿Los jets son caros? ¿Son autos? o bicicletas ... zapatos ...?

En este caso, la pregunta es: ¿los eventos son "caros" en relación con el tiempo necesario para que JobFunction se ejecute? Sería útil publicar algunas cifras absolutas: ¿cuánto tiempo dura el proceso cuando se "desenreda"? ¿Son meses o algunos femtosegundos?

¿Qué sucede con el tiempo a medida que aumenta el tamaño del subproceso? Pruebe un tamaño de grupo de 1, luego 2 y 4, etc.

Además, como ha tenido algunos problemas con los grupos de subprocesos aquí en el pasado, le sugiero que depure para contar el número de veces que su función thread en realidad se invoca ... ¿coincide con lo que esperas?

Escogiendo una figura del aire (sin saber nada sobre su sistema de destino, y suponiendo que no está haciendo nada 'enorme' en el código que no ha mostrado), esperaría que el "evento general" cada "trabajo" se medirá en microsegundos. Tal vez cien o más. Si el tiempo necesario para ejecutar el algoritmo en JobFunction no es significativamente MÁS que esta vez, entonces es probable que sus hilos le cuesten tiempo en lugar de guardarlo.

1

Dado que usted dice que es mucho más lento en paralelo de ejecución secuencial, supongo que su tiempo de procesamiento para sus 2500 iteraciones del bucle interno es pequeño (en los pocos rango de micro segundos). Entonces, no hay mucho que pueda hacer, excepto revisar su algoritmo para dividir trozos más grandes de precesión; OpenMP no ayudará y todas las demás técnicas de sincronización tampoco ayudarán, ya que todas ellas dependen fundamentalmente de los eventos (los bucles de giro no son válidos).

Por otro lado, si el tiempo de procesamiento de las 2500 iteraciones de bucle es superior a 100 microsegundos (en las PC actuales), es posible que se encuentre con limitaciones del hardware. Si su procesamiento utiliza mucho ancho de banda de memoria, dividirlo en cuatro procesadores no le dará más ancho de banda, en realidad le dará menos debido a las colisiones. También podría estar teniendo problemas con el ciclo de caché, donde cada una de las 1000 iteraciones principales se vaciará y volverá a cargar el caché de los 4 núcleos. Entonces no hay una solución única, y dependiendo de su hardware objetivo, puede que no haya ninguna.

+0

¡Gracias por los comentarios! OpenMP ayudó un poco, pero me ayudó en gran medida al permitirme deshacerme de mi grupo de hilos personalizado y confiar en algo mucho más confiable. – Wookai

+0

OpenMP probablemente fue útil porque usa el hilo actual para la ejecución. Por lo tanto, tiene un 20% menos de sincronización en su caso. También a menudo se implementa con un pequeño bucle antes de la suspensión, por lo que si la ejecución es rápida, en muchos casos puede ayudar a eliminar Eventos por completo. – Juice

0

Como se mencionó anteriormente, la cantidad de sobrecarga agregada por subprocesos depende de la cantidad relativa de tiempo que lleva realizar los "trabajos" que ha definido. Por lo tanto, es importante encontrar un equilibrio en el tamaño de los trozos de trabajo que minimice el número de piezas pero no deje a los procesadores inactivos esperando a que se complete el último grupo de cálculos.

Su enfoque de codificación ha aumentado la cantidad de trabajo de sobrecarga al buscar activamente un hilo inactivo para suministrar trabajo nuevo. El sistema operativo ya está haciendo un seguimiento de eso y haciéndolo mucho más eficientemente. Además, su función ThreadPool :: addJob() puede encontrar que todos los hilos están en uso y no pueden delegar el trabajo. Pero no proporciona ningún código de retorno relacionado con ese problema.Si no está comprobando esta condición de alguna manera y no está notando errores en los resultados, significa que siempre hay procesadores inactivos. Sugeriría reorganizar el código para que addJob() haga lo que se llama: agrega un trabajo SOLAMENTE (sin encontrar o incluso sin importar quién hace el trabajo) mientras que cada hilo de trabajo obtiene nuevo trabajo activamente cuando se hace con su trabajo existente.

Cuestiones relacionadas