Ich habe einige Code, um mehrere Millionen Datenzeilen in meiner eigenen R-Like C# DataFrame-Klasse zu verarbeiten. Es gibt eine Reihe von Parallel.ForEach-Aufrufen, um die Datenzeilen parallel zu durchlaufen. Dieser Code läuft seit über einem Jahr mit VS2013 und .NET 4.5 ohne Probleme.Garbage Collection und Parallel.ForEach Problem nach VS2015 Upgrade
Ich habe zwei Dev-Maschinen (A und B) und vor kurzem Upgrade von Maschine A zu VS2015. Ich bemerkte in der Hälfte der Zeit ein merkwürdiges zeitweiliges Einfrieren in meinem Code. Lässt es für eine lange Zeit laufen, stellt sich heraus, dass der Code schließlich fertig ist. Es dauert nur 15-120 Minuten statt 1-2 Minuten.
Versuche zu brechen Alle mit dem VS2015 Debugger aus irgendeinem Grund fehlschlagen. Also habe ich eine Reihe von Log-Anweisungen eingefügt. Es stellt sich heraus, dass dieser Einfriervorgang auftritt, wenn eine Gen2-Auflistung während einer Parallel.ForEach-Schleife vorhanden ist (Vergleichen der Anzahl der Auflistungen vor und nach jeder Parallel.ForEach-Schleife). Die gesamten zusätzlichen 13-118 Minuten werden innerhalb der Parallele verbracht. Denn jeder Schleifenanruf überschneidet sich zufällig mit einer Gen2-Sammlung (falls vorhanden). Wenn es während irgendwelcher Parallel.ForEach-Schleifen keine Gen2-Sammlungen gibt (etwa 50% der Zeit, wenn ich sie ausführe), dann ist alles in 1-2 Minuten in Ordnung.
Wenn ich den gleichen Code in VS2013 auf Maschine A ausführen, bekomme ich die gleichen Einfrieren. Wenn ich den Code in VS2013 auf Maschine B (die nie aktualisiert wurde) ausführen, funktioniert es einwandfrei. Es lief Dutzende von Stunden über Nacht ohne Einfrieren.
einige Dinge, die ich bemerkt habe/versucht:
- Die gefriert mit oder ohne den Debugger auf der Maschine angebracht passieren (ich dachte, es war etwas, mit dem VS2015 Debugger zunächst)
- Die gefriert passieren ob ich in Debug oder Release-Modus
- die gefriert bauen passieren, wenn ich .NET 4.5 oder .NET 4.6
- Ziel habe ich versucht, RyuJIT deaktivieren aber das ist nicht das gefriert
Ich ändere die Standard-GC-Einstellungen überhaupt nicht. Laut GCSettings erfolgen alle Läufe mit LatencyMode Interactive und IsServerGC als false.
Ich könnte nur vor jedem Aufruf von Parallel.ForEach zu LowLatency wechseln, aber ich würde wirklich lieber verstehen, was los ist.
Hat jemand sonst seltsame friert in Parallel.ForEach nach dem Upgrade VS2015 gesehen? Irgendwelche Ideen, was ein guter nächster Schritt wäre?
UPDATE 1: einige Beispiel-Code in die nebulöse Erklärung oben ...
Hier Hinzufügen einige Beispiel-Code, dass ich dieses Problem wird zeigen, hoffen. Dieser Code läuft in 10-12 Sekunden auf der B-Maschine konsistent. Es trifft auf eine Reihe von Gen2-Sammlungen, aber sie brauchen fast keine Zeit. Wenn ich die zwei GC-Einstellungszeilen auskommentiere, kann ich sie zwingen, keine Gen2-Sammlungen zu haben. Es ist etwas langsamer als bei 30-50 Sekunden.
Jetzt auf meiner A-Maschine dauert der Code eine zufällige Menge an Zeit. Es scheint zwischen 5 und 30 Minuten zu sein. Und es scheint schlimmer zu werden, je mehr Gen2-Sammlungen es trifft. Wenn ich die zwei GC-Einstellungszeilen auskommentiere, dauert es bei Maschine A ebenfalls 30 bis 50 Sekunden (wie bei Maschine B).
Es kann einige Anpassungen in Bezug auf die Anzahl der Zeilen und die Array-Größe erfordern, damit dies auf einem anderen Computer angezeigt wird.
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();
}
}
UPDATE 2: Nur Dinge aus den Kommentaren für zukünftige Leser ...
Dieser Hotfix verschieben: https://support.microsoft.com/en-us/kb/3088957 vollständig behebt das Problem. Ich sehe keine Probleme mit der Langsamkeit, nachdem ich mich beworben habe.
Es stellte sich heraus, dass nichts mit Parallel.ForEach zu tun hatte, glaube ich basierend auf diesem: http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx obwohl der Hotfix Parallel.ForEach aus irgendeinem Grund erwähnt.
Der nächste Schritt ein [MCVE] zu schreiben wäre (http://stackoverflow.com/help/mcve), so können wir versuchen, reproduzieren dies auf unserer Maschine und sehen, ob wir das gleiche Verhalten erfahren. Wurde dieser als x86- oder x64-Prozess ausgeführt? –
x64. Verstanden, an einem gearbeitet. Aber es ist schwer, die GCs so richtig zum Laufen zu bringen. Hatte gehofft, dass mir etwas offensichtlich fehlte. –
@MichaelCovelli Was passiert, wenn Sie GC mit 'GC.Collect()' in die Schleife zwingen? – svick