2012-04-03 13 views
8

Espero que esta sea una publicación válida aquí, es una combinación de problemas de C# y hardware.Rendimiento de C# que varía debido a la memoria

Estoy evaluando nuestro servidor porque hemos encontrado problemas con el rendimiento de nuestra biblioteca cuantitativa (escrito en C#). Simulé los mismos problemas de rendimiento con un código simple de C#, realizando un uso de memoria muy intenso.

El siguiente código está en una función que se genera a partir de un subproceso, hasta un máximo de 32 subprocesos (porque nuestro servidor tiene 4x CPUs x 8 núcleos cada uno).

Esto es todo en .Net 3.5

El problema es que nos estamos tremendamente diferentes rendimiento. Ejecuto la siguiente función 1000 veces. El tiempo promedio que tarda el código en ejecutarse puede ser, por ejemplo, 3.5s, pero el más rápido solo será 1.2s y el más lento será 7s- ¡para la misma función!

he graficado el uso de memoria en contra de los tiempos y hay doesnt parece que haya ninguna correlación con la GC patadas en.

Una cosa que me di cuenta es que cuando se ejecuta en un solo hilo de los tiempos son idénticos y hay no es una desviación salvaje También probé algoritmos de CPU y los tiempos son idénticos también. Esto nos ha hecho preguntarnos si el bus de memoria simplemente no puede hacer frente.

Me preguntaba si este podría ser otro problema de .net o C#, ¿o es algo relacionado con nuestro hardware? ¿Esta sería la misma experiencia si hubiera usado C++ o Java? Estamos utilizando 4 x Intel x7550 con 32 GB de ram. ¿Hay alguna forma de evitar este problema en general?

Stopwatch watch = new Stopwatch(); 
watch.Start(); 
List<byte> list1 = new List<byte>(); 
List<byte> list2 = new List<byte>(); 
List<byte> list3 = new List<byte>(); 


int Size1 = 10000000; 
int Size2 = 2 * Size1; 
int Size3 = Size1; 

for (int i = 0; i < Size1; i++) 
{ 
    list1.Add(57); 
} 

for (int i = 0; i < Size2; i = i + 2) 
{ 
    list2.Add(56); 
} 

for (int i = 0; i < Size3; i++) 
{ 
    byte temp = list1.ElementAt(i); 
    byte temp2 = list2.ElementAt(i); 
    list3.Add(temp); 
    list2[i] = temp; 
    list1[i] = temp2; 
} 
watch.Stop(); 

(el código es sólo la intención de subrayar la memoria)

me gustaría incluir el código de subprocesos, pero utilizó una biblioteca de subprocesos no estándar.

EDITAR: He reducido "size1" a 100000, que básicamente no utiliza mucha memoria y todavía tengo mucha inestabilidad. Esto sugiere que no es la cantidad de memoria que se transfiere, sino la frecuencia de captura de memoria.

+0

¿Se están ejecutando otros procesos durante su punto de referencia? Incluso el sistema operativo necesita tiempo de CPU. Si está utilizando todos los núcleos virtuales durante su punto de referencia, está virtualmente (perdón por el juego de palabras) garantizado que los procesos no relacionados tomarán tiempo de CPU durante su prueba. –

+5

No tenemos suficiente información para hacer otra cosa que especular. Dicho esto, mi dinero está en su "biblioteca de subprocesos no estándar" que no asigna suficientes subprocesos para ejecutar esto en paralelo. Si ejecuta 50 copias y solo asigna 20 hilos (por ejemplo), 10 iteraciones tendrán que esperar (en promedio) para que se completen otras 2 iteraciones para que se libere un hilo. Eso podría explicar las desviaciones que estás viendo. –

+8

Solo una idea: dado que parece conocer el tamaño de la lista, debe pasar eso al constructor (o simplemente usar matrices). Luego evitas las reasignaciones si las matrices subyacentes. –

Respuesta

0

La lista utiliza matrices internamente para el almacenamiento. Creo que intentará duplicar el tamaño de la matriz cada vez que alcance el límite de espacio libre en la Lista.

A medida que avanzas en el ciclo, necesita trozos más grandes y más grandes de memoria contigua para asignar las nuevas matrices a medida que la lista crece. Con un hilo, esto es bastante fácil. Con más de 2 hilos, estás compitiendo por grandes trozos de memoria contigua. Dispara el GC en momentos aleatorios a medida que las matrices se hacen más grandes y la memoria contigua es más difícil de encontrar.

+0

Hola, he cambiado las listas para los tamaños predeterminados byte [], donde el tamaño es de 10,000,000 y los tiempos para completar la función son todavía completamente al azar. Lo más rápido es 462ms, el promedio es 1192ms y el más lento es 2509ms, más del doble del promedio. – mezamorphic

1

Aquí está cumpliendo limitaciones bastante fundamentales de la máquina. Tienes muchos núcleos, pero todavía hay un solo bus de memoria. Por lo tanto, si los subprocesos realizan una gran cantidad de barajados de datos, es probable que los acelere el ancho de banda de ese solo bus. Esta es la ley de Amdahl en el trabajo.

Hay una posible optimización, depende del tipo de sistema operativo que esta máquina ejecuta. Este es un tipo de hardware de servidor, pero si tiene una versión de Windows que no es de servidor, el recolector de elementos no utilizados se ejecutará en modo de estación de trabajo. Luego puede usar el elemento <gcServer> en el archivo .config de la aplicación para solicitar la versión del servidor del recopilador. Utiliza varios montones para que los subprocesos no luchen por el bloqueo de montón de GC con tanta frecuencia cuando asignan memoria. Ymmv.

0

Asegúrese de que la configuración de tiempo de ejecución tiene gcserver = true

+0

Investigó que hizo que el proceso promediara más rápido pero no redujo la variación en los tiempos. – mezamorphic

+0

Me interesaría ver los resultados del uso de parallel.for en su código para ver el impacto de las llamadas asincrónicas que hacen que – Shay

4

No hay suficiente para seguir adelante, pero aquí hay algunas áreas para empezar a buscar:

  • La variabilidad es el resultado de estado interno GC . El GC gestiona dinámicamente los tamaños de las distintas agrupaciones. Si comienza con diferentes tamaños de grupo, obtendrá un comportamiento diferente del GC durante las ejecuciones.
  • Patrones de moiré en la programación de hilos. Dependiendo de las variaciones aleatorias en la secuencia de los hilos, podría tener patrones de disputa más o menos favorables. Si hay alguna periodicidad, eso puede llevar a un efecto amplificado similar a la interferencia constructiva.
  • Compartimiento falso. Si tiene dos hilos que alcanzan las direcciones de memoria lo suficientemente cerca como para ubicarse en el caché del procesador, verá una marcada disminución en el rendimiento ya que los procesadores tienen que pasar mucho tiempo re-sincronizando sus cachés. Dependiendo de cómo organice sus datos y asigne hilos para procesarlos, puede obtener patrones en el intercambio falso basado en variaciones al inicio.
  • Otro proceso en el sistema está consumiendo el tiempo del procesador. Es posible que desee utilizar una medida de tiempo de modo de usuario de proceso en lugar de tiempo de pared. (Hay un acceso a eso en la clase Process en alguna parte).
  • La máquina se está ejecutando cerca de su límite de memoria física completa. El intercambio en el disco se produce con un patrón aleatorio más o menos.
+1

# 3 se denomine comúnmente [uso compartido falso] (http://en.wikipedia.org/wiki/False_sharing). –

+0

@RonWarholic Gracias. Sabía que había un término para eso, simplemente no podía recordar. –

0

En este punto, parece que adivinar algo simplemente sería una conjetura. Realmente lo que necesitas es más información.

Me gustaría conectar un generador de perfiles o la configuración de algunos contadores de rendimiento de Windows:

http://support.microsoft.com/kb/300504

usted debería ser capaz de añadir algunos contadores de rendimiento centrados en el proceso. Puedes ver cuántos hilos se están creando, utilizar la memoria, etc. Tomaré algunas de las otras sugerencias aquí y mediré el escenario que estás buscando. Si transfiere los datos del contador de rendimiento a un archivo csv, incluso puede graficar los resultados bastante rápido para obtener datos buenos para realmente masticar. Si puede encontrar qué métrica está cambiando con el escenario de 1.2 contra 7, puede comenzar a hacer algunas conjeturas sobre lo que está sucediendo y continuar pulsando.

0

Llamadas sincrónicas a recursos compartidos, como la consola o el archivo Sistema, degradará significativamente el rendimiento, pero por lo que parece, este código está maximizando la CPU y las variaciones de tiempo deben ser debidas a otros procesos que solicitan tiempo de CPU.

Cuestiones relacionadas