2013-03-31 9 views
10

Ich arbeite an einer Flüssigkeitssimulation in C#. In jedem Zyklus muss ich die Geschwindigkeit der Flüssigkeit an diskreten Punkten im Raum berechnen. Als Teil dieser Berechnung benötige ich einige zehn Kilobyte für den Scratch-Space, um einige double [] -Arrays zu speichern (die genaue Größe der Arrays hängt von einigen Eingabedaten ab). Die Arrays werden nur für die Dauer der Methode benötigt, die sie verwendet, und es gibt ein paar verschiedene Methoden, die solchen Speicherplatz benötigen.Scratch-Speicher in einer verwalteten Umgebung

Wie ich es zu sehen, gibt es einige unterschiedliche Lösungen für die Kratz Arrays Konstruktion:

  1. Verwendung ‚neuer‘ Speicher aus dem Heap jedes Mal die Methode aufgerufen wird, greifen. Das war es, was ich zuerst gemacht habe, aber es bringt viel Druck auf den Müllsammler, und die paar-ms-Spikes einmal oder zweimal pro Sekunde sind wirklich nervig.

  2. Lassen Sie die Scratch-Arrays beim Aufruf der Methoden als Parameter übergeben. Problem ist, dass dies den Benutzer zwingt, sie zu verwalten, einschließlich der richtigen Größenanpassung, was ein großer Schmerz ist. Und es macht die Verwendung von mehr oder weniger Arbeitsspeicher schwierig, da es die API ändert.

  3. Verwenden Sie stackalloc in einem unsicheren Kontext, um den Arbeitsspeicher dem Programmstapel zuzuordnen. Das würde gut funktionieren, außer dass ich mit/unsafe kompilieren müsste und ständig ungeschützte Blöcke in meinen Code streuen müsste, was ich gerne vermeiden würde.

  4. Geben Sie private Arrays beim Start des Programms einmal vor. Das ist in Ordnung, außer dass ich nicht unbedingt die Größe der Arrays kenne, die ich brauche, bis ich einige der Eingabedaten sehen kann. Und es wird wirklich unordentlich, da Sie den Umfang dieser privaten Variablen nicht auf eine einzige Methode beschränken können, so dass sie den Namensraum ständig verschmutzen. Und es skaliert schlecht, da die Anzahl der Methoden, die Arbeitsspeicher benötigen, zunimmt, da ich viel Speicher zuweise, der nur einen Bruchteil der Zeit verwendet wird.

  5. Erstellen Sie eine Art zentralen Pool und ordnen Sie Arbeitsspeicher-Arrays aus dem Pool zu. Das Hauptproblem dabei ist, dass ich keine einfache Möglichkeit sehe, dynamisch große Arrays aus einem zentralen Pool zu reservieren. Ich könnte einen Start-Offset und eine Länge verwenden und den gesamten Arbeitsspeicher im Wesentlichen ein einzelnes großes Array teilen, aber ich habe viel vorhandenen Code, der double [] s annimmt. Und ich muss vorsichtig sein, um einen solchen Poolfaden sicher zu machen.

...

jemand Erfahrung mit einem ähnlichen Problem Hat? Irgendwelche Ratschläge/Lektionen, um von der Erfahrung anzubieten?

+0

Haben Sie wirklich ein paar Dutzend Kilobytes gemeint? Weil das so eine winzige Menge ist, mache ich mir keine Gedanken über die Speicherverwaltung ... –

+0

Es hört sich nicht nach viel an, aber wenn ich 2000 Zyklen/Sek. Durchführe, sind es plötzlich 60MB/Sek Der GC beginnt zu bemerken. –

+0

@JayLemmon, ich denke, dass Sie sich aus Leistungsgründen um diese Details kümmern, richtig? Wenn Ihr Projekt nicht fertig ist, schlage ich vor, dass Sie die Leistung nicht bis zum Ende kümmern. Siehe diesen Artikel über [vorzeitige Optimierung] (http://c2.com/cgi/wiki?PrematureOptimization). Wenn das Projekt abgeschlossen ist, gibt der Artikel interessante Beobachtungen auch zur __ Optimierung im Allgemeinen. Ich zitiere einen Teil: "Ein weit verbreiteter Irrtum ist, dass optimierter Code notwendigerweise komplizierter ist [...] besserer Code, der oft schneller läuft und weniger Speicher verbraucht [...]". – jay

Antwort

3

Sie können den Code wickeln, die Thesen Arrays in einer using Anweisung wie folgt zerkratzen verwendet:

using(double[] scratchArray = new double[buffer]) 
{ 
    // Code here... 
} 

Dadurch wird der Speicher explizit freigeben, indem die descructor am Ende des using-Anweisung aufrufen.

Leider scheint das obige nicht wahr! Stattdessen können Sie etwas in der Art einer Hilfsfunktion versuchen, die ein Array mit der passenden Größe zurückgibt (nächstliegende Potenz von 2 größer als die Größe), und wenn es nicht existiert, wird es erzeugt. Auf diese Weise haben Sie nur eine logarithmische Anzahl von Arrays. Wenn Sie wollten, dass es threadsicher ist, müssten Sie etwas mehr Ärger machen.

es so etwas wie folgt aussehen könnte: Thread-sichere Version:

private static Dictionary<int,double[]> scratchArrays = new Dictionary<int,double[]>(); 
/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 

Edit (pow2roundup von Algorithm for finding the smallest power of two that's greater or equal to a given value verwenden) haben Dies wird immer noch Dinge, die Müll gesammelt, aber es geht Thread spezifisch sein und sollte viel weniger Overhead sein.

[ThreadStatic] 
private static Dictionary<int,double[]> _scratchArrays; 

private static Dictionary<int,double[]> scratchArrays 
{ 
    get 
    { 
     if (_scratchArrays == null) 
     { 
      _scratchArrays = new Dictionary<int,double[]>(); 
     } 
     return _scratchArrays; 
    } 
} 

/// Round up to next higher power of 2 (return x if it's already a power of 2). 
public static int Pow2RoundUp (int x) 
{ 
    if (x < 0) 
     return 0; 
    --x; 
    x |= x >> 1; 
    x |= x >> 2; 
    x |= x >> 4; 
    x |= x >> 8; 
    x |= x >> 16; 
    return x+1; 
} 
private static double[] GetScratchArray(int size) 
{ 
    int pow2 = Pow2RoundUp(size); 
    if (!scratchArrays.ContainsKey(pow2)) 
    { 
     scratchArrays.Add(pow2, new double[pow2]); 
    } 
    return scratchArrays[pow2]; 
} 
+0

Leider disponiert! = Gesammelt :(Siehe http://stackoverflow.com/questions/655902/using-and-garbage-collection –

+0

Das ist bedauerlich. Ich werde meine Antwort mit einer anderen Idee aktualisieren. –

+0

Ah, ich habe nicht Ich weiß nichts über ThreadStatic, das macht Thread-Local-ish-Scratch-Speicher viel einfacher –

7

Ich sympathisiere mit Ihrer Situation; Als ich an Roslyn arbeitete, überlegten wir sehr sorgfältig die möglichen Leistungsprobleme, die durch den Sammeldruck bei der Zuweisung von temporär arbeitenden Arrays verursacht wurden. Die Lösung, auf die wir uns einigten, war eine Pooling-Strategie.

In einem Compiler sind die Array-Größen tendenziell klein und werden daher häufig wiederholt. In Ihrer Situation, wenn Sie große Arrays haben, dann was ich tun würde ist Toms Vorschlag zu folgen: das Verwaltungsproblem zu vereinfachen und etwas Platz zu verschwenden. Wenn Sie den Pool nach einem Array der Größe x fragen, runden Sie x auf die nächste Potenz von zwei und ordnen Sie ein Array dieser Größe zu, oder nehmen Sie ein Array aus dem Pool. Der Aufrufer bekommt ein Array, das ein bisschen zu groß ist, aber sie können geschrieben werden, um damit umzugehen. Es sollte nicht zu schwierig sein, den Pool nach einem Array mit der passenden Größe zu durchsuchen. Oder Sie könnten eine Reihe von Pools verwalten, einen Pool für Arrays der Größe 1024, einen für 2048 und so weiter.

Das Schreiben eines threadsafe-Pools ist nicht zu schwer, oder Sie können den Pool-Thread statisch machen und einen Pool pro Thread haben.

Das knifflige Bit erhält Speicher im Pool zurück. Es gibt ein paar Möglichkeiten, damit umzugehen. Erstens könnten Sie einfach verlangen, dass der Benutzer des gepoolten Speichers eine "Zurück im Pool" -Methode aufruft, wenn sie mit dem Array fertig sind, wenn sie nicht die Kosten des Sammeldrucks übernehmen wollen. Eine andere Möglichkeit besteht darin, einen Fassaden-Wrapper um das Array zu schreiben, IDisposable zu implementieren, so dass Sie "using" (*) verwenden können, und einen Finalizer zu erstellen, der das Objekt wieder in den Pool zurückbringt und es wiederbelebt. (Stellen Sie sicher, dass der Finalizer das Bit "Ich muss abgeschlossen sein" wieder aktiviert.) Finalisten, die die Auferstehung machen, machen mich nervös. Ich würde persönlich den früheren Ansatz bevorzugen, was wir in Roslyn getan haben.


(*) Ja, verletzt dies das Prinzip, dass „mit“ sollte anzeigen, dass eine nicht verwaltete Ressource an das Betriebssystem zurückgegeben wird. Im Wesentlichen behandeln wir verwalteten Speicher als eine nicht verwaltete Ressource, indem wir unsere eigene Verwaltung durchführen, also ist es nicht so schlecht.

+0

Danke, das ist genau die Art von Kriegsgeschichte, auf die ich gehofft hatte :) Ein Pool klingt wie die vernünftigste Lösung bisher, und ich nicht "Verstand", die Erinnerung so zu "verschwenden". Ich bin immer noch nicht super verkauft, weil ich darauf angewiesen bin, dass Benutzer die Ressource zurückgeben oder versuchen, sie in eine Dispose-Methode zu stopfen, aber ich denke, es ist genau das, was sie ist. –

Verwandte Themen