Reduzierung der Speichernutzung von .NET-Anwendungen?

108

Was sind einige Tipps, um die Speichernutzung von .NET-Anwendungen zu reduzieren? Betrachten Sie das folgende einfache C # -Programm.

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();
    }
}

Der Task-Manager wurde im Release- Modus für x64 kompiliert und außerhalb von Visual Studio ausgeführt. Er meldet Folgendes:

Working Set:          9364k
Private Working Set:  2500k
Commit Size:         17480k

Es ist ein bisschen besser, wenn es nur für x86 kompiliert wurde :

Working Set:          5888k
Private Working Set:  1280k
Commit Size:          7012k

Ich habe dann das folgende Programm ausprobiert, das dasselbe tut, aber versucht, die Prozessgröße nach der Laufzeitinitialisierung zu reduzieren:

class Program
{
    static void Main(string[] args)
    {
        minimizeMemory();
        Console.ReadLine();
    }

    private static void minimizeMemory()
    {
        GC.Collect(GC.MaxGeneration);
        GC.WaitForPendingFinalizers();
        SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle,
            (UIntPtr) 0xFFFFFFFF, (UIntPtr)0xFFFFFFFF);
    }

    [DllImport("kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessWorkingSetSize(IntPtr process,
        UIntPtr minimumWorkingSetSize, UIntPtr maximumWorkingSetSize);
}

Die Ergebnisse auf x86 Release außerhalb von Visual Studio:

Working Set:          2300k
Private Working Set:   964k
Commit Size:          8408k

Was ein bisschen besser ist, aber für ein so einfaches Programm immer noch übertrieben erscheint. Gibt es irgendwelche Tricks, um einen C # -Prozess etwas schlanker zu machen? Ich schreibe ein Programm, das die meiste Zeit im Hintergrund ausgeführt wird. Ich mache bereits Benutzeroberflächenmaterial in einer separaten Anwendungsdomäne, was bedeutet, dass das Benutzeroberflächenmaterial sicher entladen werden kann, aber 10 MB zu beanspruchen, wenn es nur im Hintergrund sitzt, scheint übermäßig.

PS Warum es mich interessieren würde --- (Power-) Benutzer neigen dazu, sich über diese Dinge Sorgen zu machen. Auch wenn es fast keine Auswirkungen auf die Leistung hat, neigen semi-technisch versierte Benutzer (meine Zielgruppe) dazu, über die Speichernutzung im Hintergrundanwendungen zu zischen. Sogar ich flippe aus, wenn ich sehe, dass Adobe Updater 11 MB Speicher benötigt, und fühle mich beruhigt von der beruhigenden Note von Foobar2000, die selbst beim Spielen weniger als 6 MB benötigen kann. Ich weiß, dass dieses Zeug in modernen Betriebssystemen technisch gesehen nicht so wichtig ist, aber das bedeutet nicht, dass es keinen Einfluss auf die Wahrnehmung hat.

Robert Fraser
quelle
14
Warum sollte es dich interessieren? Das private Arbeitsset ist ziemlich niedrig. Moderne Betriebssysteme werden auf die Festplatte übertragen, wenn der Speicher nicht benötigt wird. Es ist 2009. Wenn Sie nicht auf eingebetteten Systemen aufbauen, sollten Sie sich nicht um 10 MB kümmern.
Mehrdad Afshari
8
Wenn Sie .NET nicht mehr verwenden, können Sie kleine Programme verwenden. Zum Laden des .NET Frameworks müssen viele große DLLs in den Speicher geladen werden.
Adam Sills
2
Die Speicherpreise fallen exponentiell (ja, Sie können jetzt ein Dell-Heimcomputersystem mit 24 GB RAM bestellen). Sofern Ihre Anwendung nicht> 500 MB verwendet, ist eine Optimierung nicht erforderlich.
Alex
28
@LeakyCode Ich hasse es wirklich, dass moderne Programmierer so denken, dass Sie sich um die Speichernutzung Ihrer Anwendung kümmern sollten. Ich kann sagen, dass die meisten modernen Anwendungen, die hauptsächlich in Java oder C # geschrieben wurden, in Bezug auf das Ressourcenmanagement ziemlich ineffektiv sind. Dank dieser Funktion können wir 2014 auf win95 und 64 MB RAM so viele Anwendungen ausführen, wie wir es 1998 konnten ... nur 1 Instanz des Browsers frisst jetzt 2 GB RAM und die einfache IDE etwa 1 GB. Ram ist billig, aber das heißt nicht, dass Sie es verschwenden sollten.
Petr
6
@Petr Sie sollten sich um das Ressourcenmanagement kümmern. Programmierzeit ist auch eine Ressource. Bitte verallgemeinern Sie nicht die Verschwendung von 10 MB bis 2 GB.
Mehrdad Afshari

Antworten:

33
  1. Möglicherweise möchten Sie die Frage zum Stapelüberlauf .NET EXE-Speicherbedarf überprüfen .
  2. Im MSDN-Blogbeitrag Arbeitssatz! = Tatsächlicher Speicherbedarf geht es darum, den Arbeitssatz zu entmystifizieren, den Arbeitsspeicher zu verarbeiten und genaue Berechnungen Ihres gesamten RAM-Verbrauchs durchzuführen.

Ich werde nicht sagen, dass Sie den Speicherbedarf Ihrer Anwendung ignorieren sollten - offensichtlich sind kleinere und effizientere eher wünschenswert. Sie sollten jedoch Ihre tatsächlichen Bedürfnisse berücksichtigen.

Wenn Sie eine Standardanwendung für Windows Forms und WPF-Clients schreiben, die auf dem PC einer Person ausgeführt werden soll und wahrscheinlich die Hauptanwendung ist, in der der Benutzer arbeitet, können Sie die Speicherzuweisung weniger genau kennen. (Solange alles freigegeben wird.)

Um jedoch einige Leute anzusprechen, die sagen, dass sie sich keine Sorgen machen sollen: Wenn Sie eine Windows Forms-Anwendung schreiben, die in einer Terminaldienstumgebung auf einem gemeinsam genutzten Server ausgeführt wird, der möglicherweise von 10, 20 oder mehr Benutzern verwendet wird, dann ja müssen Sie unbedingt die Speichernutzung berücksichtigen. Und Sie müssen wachsam sein. Der beste Weg, dies zu beheben, ist ein gutes Datenstrukturdesign und das Befolgen von Best Practices hinsichtlich des Zeitpunkts und der Zuweisung.

John Rudy
quelle
45

.NET-Anwendungen haben im Vergleich zu nativen Anwendungen einen größeren Platzbedarf, da beide die Laufzeit und die Anwendung in den Prozess laden müssen. Wenn Sie etwas wirklich Ordentliches wollen, ist .NET möglicherweise nicht die beste Option.

Beachten Sie jedoch, dass die erforderlichen Speicherseiten, wenn Ihre Anwendung hauptsächlich im Ruhezustand ist, nicht mehr über genügend Arbeitsspeicher verfügen und daher das System die meiste Zeit nicht wirklich stark belasten.

Wenn Sie den Platzbedarf klein halten möchten, müssen Sie über die Speichernutzung nachdenken. Hier einige Ideen:

  • Reduzieren Sie die Anzahl der Objekte und achten Sie darauf, dass Sie keine Instanz länger als erforderlich festhalten.
  • Beachten Sie List<T>ähnliche Typen, die bei Bedarf die Kapazität verdoppeln, da sie zu bis zu 50% Abfall führen können.
  • Sie können Wertetypen anstelle von Referenztypen verwenden, um mehr Speicher auf dem Stapel zu erzwingen. Beachten Sie jedoch, dass der Standardstapelspeicherplatz nur 1 MB beträgt.
  • Vermeiden Sie Objekte mit mehr als 85000 Bytes, da diese zu LOH gelangen, das nicht komprimiert ist und daher leicht fragmentiert werden kann.

Das ist wahrscheinlich keine vollständige Liste, sondern nur ein paar Ideen.

Brian Rasmussen
quelle
IOW, funktionieren die gleichen Techniken, mit denen die native Codegröße reduziert wird, in .NET?
Robert Fraser
Ich denke, es gibt einige Überlappungen, aber mit nativem Code haben Sie mehr Auswahlmöglichkeiten, wenn es um die Verwendung von Speicher geht.
Brian Rasmussen
17

Eine Sache, die Sie in diesem Fall berücksichtigen müssen, sind die Speicherkosten der CLR. Die CLR wird für jeden .NET-Prozess geladen und berücksichtigt daher die Speicherüberlegungen. Für solch ein einfaches / kleines Programm werden die Kosten der CLR Ihren Speicherbedarf dominieren.

Es wäre viel lehrreicher, eine echte Anwendung zu erstellen und deren Kosten im Vergleich zu den Kosten dieses Basisprogramms anzuzeigen.

JaredPar
quelle
7

Keine konkreten Vorschläge an sich, aber Sie können sich den CLR Profiler ansehen (kostenloser Download von Microsoft).
Schauen Sie sich nach der Installation diese Anleitung an .

Von der Anleitung:

Diese Anleitung zeigt Ihnen, wie Sie mit dem CLR Profiler-Tool das Speicherzuordnungsprofil Ihrer Anwendung untersuchen. Mit CLR Profiler können Sie Code identifizieren, der Speicherprobleme verursacht, z. B. Speicherlecks und übermäßige oder ineffiziente Speicherbereinigung.

Krapfen
quelle
7

Vielleicht möchten Sie sich die Speichernutzung einer "echten" Anwendung ansehen.

Ähnlich wie bei Java gibt es unabhängig von der Programmgröße einen festen Overhead für die Laufzeit, aber der Speicherverbrauch ist nach diesem Zeitpunkt viel vernünftiger.

Eric Petroelje
quelle
4

Es gibt immer noch Möglichkeiten, die private Arbeitsmenge dieses einfachen Programms zu reduzieren:

  1. NGEN Sie Ihre Bewerbung. Dadurch werden die JIT-Kompilierungskosten aus Ihrem Prozess entfernt.

  2. Trainieren Sie Ihre Anwendung mit MPGO und reduzieren Sie die Speichernutzung .

Feng Yuan
quelle
2

Es gibt viele Möglichkeiten, Ihren Fußabdruck zu verringern.

Eine Sache, mit der Sie in .NET immer leben müssen, ist, dass das native Image Ihres IL-Codes sehr groß ist

Und dieser Code kann nicht vollständig zwischen Anwendungsinstanzen geteilt werden. Selbst NGEN- Baugruppen sind nicht vollständig statisch, sie haben noch einige kleine Teile, die JITting benötigen.

Menschen neigen auch dazu, Code zu schreiben, der den Speicher viel länger als nötig blockiert.

Ein häufig gesehenes Beispiel: Nehmen Sie einen Datenleser und laden Sie den Inhalt in eine DataTable, um ihn in eine XML-Datei zu schreiben. Sie können leicht auf eine OutOfMemoryException stoßen. OTOH, Sie könnten einen XmlTextWriter verwenden und durch den Datenleser scrollen und XmlNodes ausgeben, während Sie durch den Datenbankcursor scrollen. Auf diese Weise haben Sie nur den aktuellen Datenbankeintrag und seine XML-Ausgabe im Speicher. Was niemals (oder unwahrscheinlich) eine höhere Garbage Collection-Generation erhalten wird und somit wiederverwendet werden kann.

Das Gleiche gilt für das Abrufen einer Liste einiger Instanzen, das Ausführen einiger Aufgaben (die Tausende neuer Instanzen hervorbringen, auf die möglicherweise irgendwo verwiesen wird), und obwohl Sie sie später nicht benötigen, verweisen Sie bis nach dem Foreach auf alles. Wenn Sie Ihre Eingabeliste und Ihre temporären Nebenprodukte explizit auf Null setzen, kann dieser Speicher bereits vor dem Verlassen Ihrer Schleife wiederverwendet werden.

C # hat eine hervorragende Funktion namens Iteratoren. Sie ermöglichen das Streamen von Objekten durch Scrollen durch Ihre Eingabe und behalten nur die aktuelle Instanz bei, bis Sie die nächste erhalten. Selbst wenn Sie LINQ verwenden, müssen Sie nicht alles behalten, nur weil Sie möchten, dass es gefiltert wird.

Robert Giesecke
quelle
1

Adressierung der allgemeinen Frage im Titel und nicht der spezifischen Frage:

Wenn Sie eine COM-Komponente verwenden, die viele Daten zurückgibt (z. B. große 2xN-Arrays von Doubles) und nur einen kleinen Bruchteil benötigt, können Sie eine Wrapper-COM-Komponente schreiben, die den Speicher vor .NET verbirgt und nur die Daten zurückgibt, die vorhanden sind erforderlich.

Das habe ich in meiner Hauptanwendung getan und den Speicherverbrauch erheblich verbessert.

Peter Mortensen
quelle
0

Ich habe festgestellt, dass die Verwendung der SetProcessWorkingSetSize- oder EmptyWorkingSet-APIs, um Speicherseiten in einem lang laufenden Prozess regelmäßig auf die Festplatte zu zwingen, dazu führen kann, dass der gesamte verfügbare physische Speicher auf einem Computer effektiv verschwindet, bis der Computer neu gestartet wird. Wir hatten eine .NET-DLL in einen nativen Prozess geladen, der die EmptyWorkingSet-API (eine Alternative zur Verwendung von SetProcessWorkingSetSize) verwendet, um den Arbeitssatz nach einer speicherintensiven Aufgabe zu reduzieren. Ich stellte fest, dass ein Computer nach einem Tag bis zu einer Woche eine 99% ige physische Speichernutzung im Task-Manager anzeigt, während keine Prozesse eine signifikante Speichernutzung aufweisen. Bald darauf reagierte der Computer nicht mehr und erforderte einen harten Neustart. Bei diesen Computern handelte es sich um mehr als 2 Dutzend Windows Server 2008 R2- und 2012 R2-Server, die sowohl auf physischer als auch auf virtueller Hardware ausgeführt wurden.

Möglicherweise hatte das Laden des .NET-Codes in einen nativen Prozess etwas damit zu tun, aber die Verwendung von EmptyWorkingSet (oder SetProcessWorkingSetSize) erfolgt auf eigenes Risiko. Verwenden Sie es möglicherweise nur einmal nach dem ersten Start Ihrer Anwendung. Ich habe beschlossen, den Code zu deaktivieren und den Garbage Collector die Speichernutzung selbst verwalten zu lassen.

Stuart Welch
quelle