34

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
beeinflußte

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.

+4

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? –

+0

x64. Verstanden, an einem gearbeitet. Aber es ist schwer, die GCs so richtig zum Laufen zu bringen. Hatte gehofft, dass mir etwas offensichtlich fehlte. –

+0

@MichaelCovelli Was passiert, wenn Sie GC mit 'GC.Collect()' in die Schleife zwingen? – svick

Antwort

5

Es sieht aus wie das Problem nun behoben worden ist, siehe http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx

+0

Danke! Werde es versuchen, sobald der Hotfix da draußen ist und melden Sie sich zurück. –

+0

Dieser Hotfix: https://support.microsoft.com/en-us/kb/3088957 wurde gerade veröffentlicht und das Problem vollständig behoben. –

+0

Die Hotfix-Version unterscheidet sich je nach der Windows-Version. Basierend auf einem Kommentar von http://blogs.msdn.com/b/maoni/archive/2015/08/12/gen2-free-list-changes-in-clr-4-6-gc.aspx, glaube ich, dass wir habe folgendes. Für Windows Vista, Windows 7, Windows Server 2008 und Windows Server 2008 R2: 3088957. Für Windows 8 und Windows Server 2012: 3088955. Für Windows 8.1 und Windows Server 2012 R2: 3088956. Für Windows 10: Kein Hotfix verfügbar. –

26

Dies funktioniert in der Tat übermäßig schlecht, die Hintergrund-GC tut Ihnen hier nicht gefallen. Als erstes habe ich bemerkt, dass Parallel.ForEach() zu viele Aufgaben verwendet. Der Threadpool-Manager interpretiert das Thread-Verhalten als "festgefahren durch E/A" und startet zusätzliche Threads. Dies macht das Problem noch schlimmer. Umgehung dafür ist:

var options = new ParallelOptions(); 
options.MaxDegreeOfParallelism = Environment.ProcessorCount; 

Parallel.ForEach(dataFrame, options, dr => { 
    // etc.. 
} 

Dies gibt einen besseren Einblick in was das Programm von der neuen Diagnose-Hub in VS2015 schmerzt. Es dauert nicht lange für nur einen einzigen Kern, der jede Arbeit tut, einfach, von der CPU-Nutzung zu erzählen. Mit gelegentlichen Spitzen halten sie nicht sehr lange und fallen mit einem orangefarbenen GC-Zeichen zusammen. Wenn Sie sich die GC-Marke genauer ansehen, sehen Sie, dass es eine gen # 1 Sammlung ist. Nehmen Sie eine sehr lange Zeit, etwa 6 Sekunden auf meiner Maschine.

Eine Gen # 1 Sammlung dauert natürlich nicht so lange, was Sie hier sehen, ist die Gen # 1 Sammlung, die darauf wartet, dass der Hintergrund-GC seine Arbeit beendet. Mit anderen Worten, es ist tatsächlich der Hintergrund-GC, der 6 Sekunden dauert. Hintergrund-GC kann nur dann effektiv sein, wenn der Speicherplatz in den Segmenten gen # 0 und gen # 1 groß genug ist, um keine Gen # 2-Sammlung zu erfordern, während der Hintergrund-GC ausgelagert wird. Nicht die Art, wie diese App funktioniert, es isst Speicher mit einer sehr hohen Rate. Die kleine Spitze, die Sie sehen, besteht darin, mehrere Aufgaben zu entsperren und Arrays wieder zuordnen zu können. Schnell zum Stillstand kommen, wenn eine Gen # 1-Sammlung erneut auf den Hintergrund-GC warten muss.

Bemerkenswert ist, dass das Zuweisungsmuster dieses Codes dem GC sehr unfreundlich ist. Es verschachtelt langlebige Arrays (dr.DerivedValues) mit kurzlebigen Arrays (TempArray). Wenn Sie dem GC viel Arbeit geben, wenn er den Heap komprimiert, wird jedes einzelne zugewiesene Array verschoben.

Der offensichtliche Fehler in der .NET 4.6 GC ist, dass die Hintergrundsammlung den Heap nie effektiv zu komprimieren scheint. Es sieht wie es macht den Job immer und immer wieder, als ob die vorherige Sammlung überhaupt nicht kompakt. Ob das Design oder ein Bug schwer zu erkennen ist, ich habe keine saubere 4.5 Maschine mehr. Ich lehne mich definitiv dem Käfer zu. Sie sollten dieses Problem unter connect.microsoft.com melden, damit Microsoft es sich anschaut.


Eine Abhilfe ist sehr einfach zu bekommen, alles, was Sie tun müssen, ist die peinliche inter Weggang von lang- und kurzlebige Objekte zu verhindern. Was tun Sie, indem Sie sie vorab zuweisen:

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(); 
    }); 

Und natürlich, indem Hintergrund-GC vollständig deaktiviert.


UPDATE: GC Fehler in this blog post bestätigt. Fix kommt bald.


UPDATE: a hotfix was released.


UPDATE: fixed in .NET 4.6.1

+0

Danke für einen Blick. Wird als die Antwort markieren, wenn es nach ein oder zwei Tagen keine anderen gibt. Ich stimme zu, dass diese Instanz leicht zu optimieren ist. Ich habe nur mit unnötigen Zuweisungen herumgespielt, bis ich etwas gefunden habe, um zu demonstrieren, was ich in meinem Code sah. Der Unterschied zwischen .NET 4.5 und 4.6 überrascht mich hier am meisten. Wird das Problem bei connect.microsoft.com melden. Vielen Dank! –

+2

@MichaelCovelli Bitte posten Sie den Microsoft Connect-Link hier, nachdem Sie ihn gemeldet haben, damit wir das Problem auch verfolgen können. – cremor

+2

Veröffentlicht unter https://connect.microsoft.com/VisualStudio/feedback/details/1621480 –

10

Wir (und andere Benutzer) haben ein ähnliches Problem aufgetreten. Wir haben daran gearbeitet, indem wir Hintergrund-GC in der app.config der Anwendung deaktiviert haben. Bitte beachten Sie die Diskussion in den Kommentaren https://connect.microsoft.com/VisualStudio/Feedback/Details/1594775.

app.config für gcConcurrent (nicht gleichzeitige Workstation GC)

<?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> 

Sie können auch auf den Server GC wechseln, obwohl dieser Ansatz mehr Speicher zu verwenden (auf einer ungesättigten Maschine zu sein scheint?).

<?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> 
+0

Vielen Dank! Werde es ausprobieren. Sicher klingt das gleiche Problem. –

+0

Beide dieser Problemumgehung beheben es. Die Umstellung auf den Server-GC verbraucht mehr Speicher, verkürzt jedoch die Ausführungszeit auf meinem Computer auf 5 Sekunden. Wenn Sie "gcConcurrent" auf "false" setzen, dauert die App etwa 10 Sekunden - die gleiche Menge wie in .NET 4.5 in VS2013. –