c# garbage-collection visual-studio-2015 parallel.foreach .net-4.6

c# - Garbage Collection y Parallel.ForEach Issue después de la actualización de VS2015



garbage-collection visual-studio-2015 (3)

Esto de hecho funciona excesivamente mal, el GC de fondo no te está favoreciendo aquí. Lo primero que noté es que Parallel.ForEach () está usando demasiadas tareas. El administrador de subprocesos malinterpreta el comportamiento del subproceso como "empantanado por E / S" e inicia subprocesos adicionales. Esto empeora el problema. La solución para eso es:

var options = new ParallelOptions(); options.MaxDegreeOfParallelism = Environment.ProcessorCount; Parallel.ForEach(dataFrame, options, dr => { // etc.. }

Esto proporciona una mejor idea de lo que le sucede al programa desde el nuevo centro de diagnóstico en VS2015. No toma mucho tiempo para que solo un núcleo haga cualquier trabajo, fácil de decir por el uso de la CPU. Con picos ocasionales, no duran mucho, coincidiendo con una marca naranja GC. Cuando le echas un vistazo más de cerca a la marca GC, ves que es una colección gen # 1 . Tomando un tiempo muy largo, aproximadamente 6 segundos en mi máquina.

Por supuesto, una colección gen # 1 no lleva tanto tiempo, lo que se ve aquí es la colección gen # 1 esperando que el GC de fondo termine su trabajo. En otras palabras, es en realidad el GC de fondo que tarda 6 segundos. El GC de fondo solo puede ser efectivo si el espacio en los segmentos gen # 0 y gen # 1 es lo suficientemente grande como para no requerir una colección gen # 2 mientras el GC de fondo se desplaza. No del modo en que funciona esta aplicación, consume memoria a un ritmo muy elevado. El pequeño pico que ves es que varias tareas se desbloquean, pudiendo asignar matrices de nuevo. Rápidamente se detiene cuando una colección gen # 1 tiene que esperar nuevamente a la GC de fondo.

Es notable que el patrón de asignación de este código es muy desagradable para el GC. Intercala matrices de larga vida (dr.DerivedValues) con matrices de corta duración (tempArray). Si se le da mucho trabajo al GC cuando compacta el montón, cada conjunto asignado se va a mover.

La falla aparente en el GC .NET 4.6 es que la colección de fondo nunca parece compactar efectivamente el montón. Parece que hace el trabajo una y otra vez, como si la colección anterior no se compactara en absoluto. Si esto es por diseño o un error es difícil de decir, ya no tengo una máquina limpia 4.5. Ciertamente me estoy inclinando hacia el error. Debería informar este problema en connect.microsoft.com para que Microsoft lo revise.

Una solución es muy fácil de conseguir, todo lo que tiene que hacer es evitar la incómoda intercalación de objetos de vida larga y corta. Lo que haces al preasignarlos:

for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { Id = i, Value = r.NextDouble(), DerivedValues = new double[tempArraySize] }); ... Parallel.ForEach(dataFrame, options, dr => { var array = dr.DerivedValues; for (int j = 0; j < array.Length; j++) array[j] = Math.Pow(dr.Value, j); dr.DerivedValuesSum = array.Sum(); });

Y, por supuesto, desactivando completamente el GC de fondo.

ACTUALIZACIÓN: error de GC confirmado en http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx . Arreglar pronto

ACTUALIZACIÓN: https://support.microsoft.com/en-us/kb/3088957 .

ACTUALIZACIÓN: arreglado en .NET 4.6.1

Tengo un código para procesar varios millones de filas de datos en mi propia clase C DataFrame de tipo R. Hay una serie de llamadas Paralela.ForEach para iterar sobre las filas de datos en paralelo. Este código se ha estado ejecutando durante más de un año utilizando VS2013 y .NET 4.5 sin problemas.

Tengo dos máquinas de desarrollo (A y B) y recientemente actualicé la máquina A a VS2015. Comencé a notar un extraño congelamiento intermitente en mi código la mitad del tiempo. Dejándolo funcionar durante mucho tiempo, resulta que el código finalmente termina. Solo toma 15-120 minutos en lugar de 1-2 minutos.

Los intentos de romper todos utilizando el depurador VS2015 siguen fallando por alguna razón. Así que inserté un montón de instrucciones de registro. Resulta que esta congelación se produce cuando hay una colección Gen2 durante un ciclo Parallel.ForEach (que compara el recuento de recopilación antes y después de cada ciclo Parallel.ForEach). Los 13-118 minutos adicionales se gastan en cualquier llamada paralela de Parallel.ForEach que se solape con una colección Gen2 (si corresponde). Si no hay colecciones Gen2 durante ningún bucle Parallel.ForEach (aproximadamente el 50% del tiempo cuando lo ejecuto), entonces todo termina bien en 1-2 minutos.

Cuando ejecuto el mismo código en VS2013 en la Máquina A, recibo las mismas congelaciones. Cuando ejecuto el código en VS2013 en la Máquina B (que nunca se actualizó), funciona perfectamente. Pasó docenas de tiempo durante la noche sin congelación.

Algunas cosas que he notado / probado:

  • Las congelaciones ocurren con o sin el depurador conectado en la Máquina A (supuse que era algo con el depurador VS2015 al principio)
  • Las congelaciones ocurren ya sea que construya en modo Debug o Release
  • Las congelaciones ocurren si apunto .NET 4.5 o .NET 4.6
  • Intenté desactivar RyuJIT, pero eso no afectó a las congelaciones

No estoy cambiando la configuración del GC por defecto. De acuerdo con GCSettings, todas las ejecuciones están ocurriendo con LatencyMode Interactive e IsServerGC como falsas.

Podría cambiar a LowLatency antes de cada llamada a Parallel.ForEach, pero realmente preferiría entender lo que está pasando.

¿Alguien más ha visto heladas extrañas en Parallel.ForEach después de la actualización de VS2015? ¿Alguna idea sobre cuál sería el siguiente paso?

ACTUALIZACIÓN 1: Agregar un código de muestra a la explicación nebulosa anterior ...

Aquí hay un código de muestra que espero demuestre este problema. Este código se ejecuta en 10-12 segundos en la máquina B, de forma consistente. Encuentra una cantidad de colecciones de Gen2, pero casi no tardan en absoluto. Si elimino el comentario de las dos líneas de configuración del GC, puedo forzarlo a que no tenga colecciones de Gen2. Es algo más lento que a los 30-50 segundos.

Ahora en mi máquina A, el código toma una cantidad aleatoria de tiempo. Parece ser entre 5 y 30 minutos. Y parece empeorar, más colecciones de Gen2 encuentra. Si descomiento las dos líneas de configuración del GC, la Máquina A también tarda de 30 a 50 segundos (igual que la Máquina B).

Podría requerir algunos ajustes en términos de la cantidad de filas y el tamaño de la matriz para que esto aparezca en otra máquina.

using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; using System.Runtime; public class MyDataRow { public int Id { get; set; } public double Value { get; set; } public double DerivedValuesSum { get; set; } public double[] DerivedValues { get; set; } } class Program { static void Example() { const int numRows = 2000000; const int tempArraySize = 250; var r = new Random(); var dataFrame = new List<MyDataRow>(numRows); for (int i = 0; i < numRows; i++) dataFrame.Add(new MyDataRow { Id = i, Value = r.NextDouble() }); Stopwatch stw = Stopwatch.StartNew(); int gcs0Initial = GC.CollectionCount(0); int gcs1Initial = GC.CollectionCount(1); int gcs2Initial = GC.CollectionCount(2); //GCSettings.LatencyMode = GCLatencyMode.LowLatency; Parallel.ForEach(dataFrame, dr => { double[] tempArray = new double[tempArraySize]; for (int j = 0; j < tempArraySize; j++) tempArray[j] = Math.Pow(dr.Value, j); dr.DerivedValuesSum = tempArray.Sum(); dr.DerivedValues = tempArray.ToArray(); }); int gcs0Final = GC.CollectionCount(0); int gcs1Final = GC.CollectionCount(1); int gcs2Final = GC.CollectionCount(2); stw.Stop(); //GCSettings.LatencyMode = GCLatencyMode.Interactive; Console.Out.WriteLine("ElapsedTime = {0} Seconds ({1} Minutes)", stw.Elapsed.TotalSeconds, stw.Elapsed.TotalMinutes); Console.Out.WriteLine("Gcs0 = {0} = {1} - {2}", gcs0Final - gcs0Initial, gcs0Final, gcs0Initial); Console.Out.WriteLine("Gcs1 = {0} = {1} - {2}", gcs1Final - gcs1Initial, gcs1Final, gcs1Initial); Console.Out.WriteLine("Gcs2 = {0} = {1} - {2}", gcs2Final - gcs2Initial, gcs2Final, gcs2Initial); Console.Out.WriteLine("Press Any Key To Exit..."); Console.In.ReadLine(); } static void Main(string[] args) { Example(); } }

ACTUALIZACIÓN 2: solo para sacar las cosas de los comentarios para futuros lectores ...

Esta revisión: https://support.microsoft.com/en-us/kb/3088957 soluciona totalmente el problema. No veo ningún problema de lentitud luego de presentar la solicitud.

Resultó no tener nada que ver con Parallel.ForEach creo basado en esto: http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx aunque la revisión menciona Parallel.ForEach por algún motivo.


Nosotros (y otros usuarios) hemos encontrado un problema similar. Hemos solucionado el problema desactivando GC de fondo en la aplicación app.config. Consulte la discusión en los comentarios de https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775 .

app.config para gcConcurrent (estación de trabajo GC no concurrente)

<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" /> </startup> <runtime> <gcConcurrent enabled="false" /> </runtime>

También puede cambiar al servidor GC, aunque este enfoque parece usar más memoria (¿en una máquina insaturada?).

<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1" /> </startup> <runtime> <gcServer enabled="true" /> </runtime> </configuration>