2010-05-25 11 views
15

Tengo un programa con muchos cálculos independientes, así que decidí paralelizarlo.Comprensión de los resultados de perfilado en paralelo VS2010 C#

Uso Parallel.For/Each.

Los resultados fueron buenos para una máquina de doble núcleo: la utilización de la CPU de aproximadamente 80% -90% la mayor parte del tiempo. Sin embargo, con una máquina dual Xeon (es decir, 8 núcleos) obtengo solo un 30% -40% de utilización de la CPU, aunque el programa pasa bastante tiempo (a veces más de 10 segundos) en las secciones paralelas, y lo veo emplea aproximadamente 20-30 hilos más en esas secciones en comparación con las secciones en serie. Cada hilo tarda más de 1 segundo en completarse, por lo que no veo ninguna razón para que no funcionen en paralelo, a menos que haya un problema de sincronización.

Utilicé el generador de perfiles integrado de VS2010, y los resultados son extraños. Aunque utilizo bloqueos solo en un lugar, el generador de perfiles informa que aproximadamente el 85% del tiempo del programa se usa en sincronización (también 5-7% de reposo, 5-7% de ejecución, menos 1% de IO).

El código de bloqueo es solo una caché (un diccionario) obtener/añadir:

bool esn_found; 
lock (lock_load_esn) 
    esn_found = cache.TryGetValue(st, out esn); 
if(!esn_found) 
{ 
    esn = pData.esa_inv_idx.esa[term_idx]; 
    esn.populate(pData.esa_inv_idx.datafile); 
    lock (lock_load_esn) 
    { 
     if (!cache.ContainsKey(st)) 
      cache.Add(st, esn); 
    } 
} 

lock_load_esn es un miembro estático de la clase de tipo Object.
esn.populate lee de un archivo usando un StreamReader separado para cada hilo.

Sin embargo, cuando presiono el botón Sincronización para ver qué causa la mayor demora, veo que el perfilador informa líneas que son líneas de entrada de función, y no informa las secciones bloqueadas.
Ni siquiera informa la función que contiene el código anterior (recordatorio: el único bloqueo en el programa) como parte del perfil de bloqueo con un nivel de ruido del 2%. Con el nivel de ruido al 0% informa todas las funciones del programa, lo que no entiendo por qué cuentan como sincronizaciones de bloqueo.

Así que mi pregunta es: ¿qué está pasando aquí?
¿Cómo puede ser que el 85% del tiempo se gaste en la sincronización?
¿Cómo averiguo cuál es realmente el problema con las secciones paralelas de mi programa?

Gracias.

actualización: Después de perforar hacia abajo en los hilos (utilizando el visualizador extremadamente útil) descubrí que la mayor parte del tiempo de sincronización se gastaron en la espera de la rosca GC para completar las asignaciones de memoria, y se necesitaban que las asignaciones frecuentes porque de las estructuras de datos genéricas cambian el tamaño de las operaciones.

Tendré que ver cómo inicializar mis estructuras de datos para que asignen suficiente memoria en la inicialización, posiblemente evitando esta carrera para el hilo GC.

Voy a informar los resultados más tarde hoy.

Actualización: Parece que las asignaciones de memoria fueron la causa del problema. Cuando utilicé las capacidades iniciales para todos los diccionarios y listas en la clase de ejecución paralela, el problema de sincronización fue menor. Ahora tenía solo un 80% de tiempo de sincronización, con picos del 70% de utilización de la CPU (los picos anteriores eran solo del 40%).

Perforé aún más en cada hilo y descubrí que ahora muchas llamadas a asignar GC se hicieron para asignar objetos pequeños que no formaban parte de los diccionarios grandes.

Resolví este problema al proporcionar a cada subproceso un grupo de objetos preasignados, que utilizo en lugar de llamar a la función "nueva".

Así que básicamente implementé un grupo separado de memoria para cada hilo, pero de una manera muy cruda, que consume mucho tiempo y realmente no es muy buena - Todavía tengo que usar un montón de nuevo para la inicialización de estos objetos, solo ahora lo hago una vez globalmente y hay menos contención en el hilo del GC, incluso cuando tengo que aumentar el tamaño del grupo.

Pero definitivamente no es una solución que me guste, ya que no se generaliza fácilmente y no me gustaría escribir mi propio administrador de memoria.
¿Hay alguna manera de decirle a .NET que asigne una cantidad predefinida de memoria para cada subproceso y luego tome todas las asignaciones de memoria del grupo local?

+0

"así que no veo ninguna razón para que trabajen en paralelo" - ¿Echaste de menos un 'no'? –

+0

Para completar, ¿qué tipos son lock_load_esn y caché? Y el caché es un miembro estático, ¿verdad? –

+0

Vaya, olvidé el "no", gracias. lock_load_esn es Object (es decir, objeto estático lock_load_esn = new object()) y cache es un contenedor Dictionary que no hace mucho más que lo que un diccionario hace con los métodos TryGetValue/ContainsKey/Add, realmente. – Haggai

Respuesta

4

¿Se puede asignar menos?

He tenido un par de experiencias similares, mirando el mal desempeño y descubriendo que el corazón del problema era el GC. En cada caso, sin embargo, descubrí que accidentalmente tenía una hemorragia en la memoria en un bucle interno, asignando toneladas de objetos temporales innecesariamente. Le doy una mirada cuidadosa al código y veo si hay asignaciones que puede eliminar. Creo que es raro que los programas "necesiten" asignar mucho en bucles internos.

+0

Creo que podría asignar menos. Pero esto significaría, por ejemplo, implementando árboles de estructuras complejas como matrices multidimensionales (en lugar de asignar cada nodo por separado), junto con otras implementaciones no intuitivas de estructuras de datos. Esto podría hacerse, pero, francamente, no creo que esto deba ser una solución general de contención de asignación de memoria. – Haggai

+0

(difícil de especular sin saber más acerca de los detalles de estos algoritmos/estructuras de datos.) – Brian

+2

BTW, ¿ha ejecutado el generador de perfiles de memoria? En ocasiones apunta a errores graves, como ver que se asignan mil millones de cadenas, y darse cuenta de 'whoops', en lugar de usar + en cadenas, debería usar un StringBuilder, o algo por el estilo. Me gustaría saber exactamente qué estructuras de datos se están asignando y luego descartar rápidamente dentro del ciclo interno. – Brian

Cuestiones relacionadas