Zmniejszenie zużycia pamięci przez aplikacje .NET?

108

Jakie są wskazówki, jak zmniejszyć zużycie pamięci przez aplikacje .NET? Rozważmy następujący prosty program w języku C #.

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

Skompilowany w trybie wydania dla x64 i działający poza programem Visual Studio, menedżer zadań raportuje następujące informacje:

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

Trochę lepiej, jeśli jest skompilowany tylko dla x86 :

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

Następnie wypróbowałem następujący program, który robi to samo, ale próbuje zmniejszyć rozmiar procesu po zainicjowaniu środowiska wykonawczego:

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

Wyniki w wersji x86 poza programem Visual Studio:

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

Co jest trochę lepsze, ale nadal wydaje się przesadne w przypadku tak prostego programu. Czy są jakieś sztuczki, które sprawią, że proces C # będzie nieco odchudzony? Piszę program, który przez większość czasu działa w tle. Już robię wszelkie rzeczy związane z interfejsem użytkownika w oddzielnej domenie aplikacji, co oznacza, że ​​elementy interfejsu użytkownika można bezpiecznie rozładować, ale zajmowanie 10 MB, gdy po prostu siedzi w tle, wydaje się nadmierne.

PS Co mnie to obchodzi --- Użytkownicy (Power) zwykle martwią się takimi rzeczami. Nawet jeśli nie ma to prawie żadnego wpływu na wydajność, średnio zaawansowani technicznie użytkownicy (moja grupa docelowa) mają tendencję do brzydkich ataków na temat wykorzystania pamięci aplikacji w tle. Nawet ja wariuję, gdy widzę, że Adobe Updater zajmuje 11 MB pamięci i czuję się uspokojony przez uspokajający dotyk Foobar2000, który może zająć poniżej 6 MB nawet podczas gry. Wiem, że w nowoczesnych systemach operacyjnych te rzeczy naprawdę nie mają większego znaczenia technicznie, ale to nie znaczy, że nie mają wpływu na percepcję.

Robert Fraser
źródło
14
Dlaczego by cię to obchodziło? Prywatny zestaw do pracy jest dość niski. Nowoczesne systemy operacyjne będą przesyłać strony na dysk, jeśli pamięć jest niepotrzebna. Jest rok 2009. Jeśli nie tworzysz rzeczy na systemach wbudowanych, nie powinieneś przejmować się 10 MB.
Mehrdad Afshari
8
Przestań używać .NET i możesz mieć małe programy. Aby załadować platformę .NET, do pamięci trzeba załadować wiele dużych bibliotek DLL.
Adam Sills
2
Ceny pamięci spadają wykładniczo (tak, teraz możesz zamówić domowy system komputerowy Dell z 24 GB pamięci RAM). Chyba że aplikacja korzysta z optymalizacji> 500 MB, jest niepotrzebna.
Alex
28
@LeakyCode Naprawdę nienawidzę tego, że współcześni programiści myślą w ten sposób, że powinieneś dbać o wykorzystanie pamięci swojej aplikacji. Mogę powiedzieć, że większość współczesnych aplikacji, napisanych głównie w javie lub c # jest dość nieefektywna jeśli chodzi o zarządzanie zasobami, a dzięki temu w 2014 roku możemy uruchomić tyle aplikacji ile mogliśmy w 1998 roku na win95 i 64mb pamięci RAM ... tylko 1 instancja przeglądarki zjada teraz 2 GB pamięci RAM, a proste IDE około 1 GB. Ram jest tani, ale to nie znaczy, że powinieneś go marnować.
Petr
6
@Petr Powinieneś zadbać o zarządzanie zasobami. Czas programisty również jest zasobem. Nie uogólniaj marnowania 10 MB do 2 GB.
Mehrdad Afshari

Odpowiedzi:

33
  1. Możesz sprawdzić pytanie dotyczące przepełnienia stosu .NET EXE .
  2. Wpis na blogu MSDN Zestaw roboczy! = Rzeczywisty ślad pamięci dotyczy demistyfikacji zestawu roboczego, pamięci procesowej i wykonywania dokładnych obliczeń całkowitego zużycia pamięci RAM.

Nie powiem, że powinieneś ignorować ślad pamięci swojej aplikacji - oczywiście mniejsze i wydajniejsze są zwykle pożądane. Należy jednak rozważyć, jakie są Twoje rzeczywiste potrzeby.

Jeśli piszesz standardowe aplikacje klienckie Windows Forms i WPF, które są przeznaczone do uruchamiania na komputerze osobistym i prawdopodobnie będą podstawową aplikacją, w której działa użytkownik, możesz uciec od braku orientacji w alokacji pamięci. (O ile wszystko zostanie zwolnione).

Jednak, aby zwrócić się do niektórych osób, które mówią, że nie należy się tym martwić: jeśli piszesz aplikację Windows Forms, która będzie działać w środowisku usług terminalowych, na serwerze współdzielonym prawdopodobnie używanym przez 10, 20 lub więcej użytkowników, to tak , bezwzględnie musisz wziąć pod uwagę użycie pamięci. I będziesz musiał być czujny. Najlepszym sposobem rozwiązania tego problemu jest dobry projekt struktury danych i przestrzeganie najlepszych praktyk dotyczących tego, kiedy i co przydzielasz.

John Rudy
źródło
45

Aplikacje .NET będą zajmować więcej miejsca w porównaniu z aplikacjami natywnymi, ponieważ oba muszą ładować środowisko wykonawcze i aplikację w trakcie procesu. Jeśli chcesz czegoś naprawdę uporządkowanego, .NET może nie być najlepszym rozwiązaniem.

Należy jednak pamiętać, że jeśli aplikacja w większości śpi, niezbędne strony pamięci zostaną wymienione z pamięci, a zatem przez większość czasu nie będą tak bardzo obciążać systemu.

Jeśli chcesz, aby zajmował mało miejsca, musisz pomyśleć o wykorzystaniu pamięci. Oto kilka pomysłów:

  • Zmniejsz liczbę obiektów i nie zatrzymuj żadnego wystąpienia dłużej niż jest to wymagane.
  • Należy pamiętać o List<T>podobnych typach, które w razie potrzeby podwajają pojemność, ponieważ mogą prowadzić do 50% strat.
  • Można rozważyć użycie typów wartości zamiast typów referencyjnych, aby wymusić więcej pamięci na stosie, ale należy pamiętać, że domyślna wielkość stosu to tylko 1 MB.
  • Unikaj obiektów o rozmiarze większym niż 85000 bajtów, ponieważ trafią one do LOH, który nie jest skompaktowany i dlatego może łatwo ulec fragmentacji.

To prawdopodobnie nie jest wyczerpująca lista, ale tylko kilka pomysłów.

Brian Rasmussen
źródło
IOW, te same rodzaje technik, które działają w celu zmniejszenia rozmiaru kodu natywnego, działają w .NET?
Robert Fraser
Wydaje mi się, że pewne elementy się pokrywają, ale z kodem natywnym masz większy wybór, jeśli chodzi o użycie pamięci.
Brian Rasmussen
17

Jedną rzeczą, którą należy wziąć pod uwagę w tym przypadku, jest koszt pamięci CLR. Środowisko CLR jest ładowane dla każdego procesu .Net, a zatem uwzględnia kwestie pamięci. W przypadku tak prostego / małego programu koszt CLR zdominuje zużycie pamięci.

Znacznie bardziej pouczające byłoby skonstruowanie rzeczywistej aplikacji i porównanie jej kosztów z kosztami tego programu podstawowego.

JaredPar
źródło
7

Brak konkretnych sugestii jako takich, ale możesz rzucić okiem na CLR Profiler (bezpłatnie do pobrania od firmy Microsoft).
Po zainstalowaniu spójrz na tę stronę z instrukcjami .

Od poradnika:

W tym poradniku pokazano, jak używać narzędzia CLR Profiler do badania profilu alokacji pamięci aplikacji. Za pomocą narzędzia CLR Profiler można zidentyfikować kod, który powoduje problemy z pamięcią, takie jak wycieki pamięci i nadmierne lub nieefektywne wyrzucanie elementów bezużytecznych.

Pączek
źródło
7

Może zechcesz przyjrzeć się wykorzystaniu pamięci przez „prawdziwą” aplikację.

Podobnie jak w Javie, istnieje pewna stała ilość narzutów dla środowiska wykonawczego, niezależnie od rozmiaru programu, ale po tym czasie zużycie pamięci będzie znacznie bardziej rozsądne.

Eric Petroelje
źródło
4

Nadal istnieją sposoby na ograniczenie prywatnego zestawu roboczego tego prostego programu:

  1. NGEN swoją aplikację. Eliminuje to koszt kompilacji JIT z procesu.

  2. Trenuj swoją aplikację przy użyciu MPGO, zmniejszając zużycie pamięci, a następnie NGEN ją.

Feng Yuan
źródło
2

Istnieje wiele sposobów na zmniejszenie zajmowanego miejsca.

Jedną rzeczą, z którą zawsze będziesz musiał żyć w .NET, jest to, że rozmiar natywnego obrazu twojego kodu IL jest ogromny

Ten kod nie może być w pełni współdzielony między instancjami aplikacji. Nawet zespoły utworzone przez NGEN nie są całkowicie statyczne, wciąż mają kilka małych części, które wymagają JITowania.

Ludzie mają również tendencję do pisania kodu, który blokuje pamięć znacznie dłużej niż to konieczne.

Często spotykany przykład: pobranie Datareadera, załadowanie zawartości do DataTable tylko po to, aby zapisać ją do pliku XML. Możesz łatwo natknąć się na wyjątek OutOfMemoryException. OTOH, możesz użyć XmlTextWriter i przewijać Datareader, emitując XmlNodes podczas przewijania kursora bazy danych. W ten sposób w pamięci masz tylko bieżący rekord bazy danych i jego dane wyjściowe XML. Który nigdy (lub jest mało prawdopodobne), aby uzyskać wyższą generację wyrzucania elementów bezużytecznych, a zatem może być ponownie użyty.

To samo dotyczy pobierania listy niektórych instancji, robienia pewnych rzeczy (co powoduje pojawienie się tysięcy nowych instancji, które mogą zostać gdzieś przywołane) i nawet jeśli nie potrzebujesz ich później, nadal odwołujesz się do wszystkiego, aż do zakończenia foreach. Jawne zerowanie listy wejściowej i tymczasowych produktów ubocznych oznacza, że ​​pamięć ta może być ponownie wykorzystana nawet przed wyjściem z pętli.

C # ma doskonałą funkcję zwaną iteratorami. Umożliwiają one przesyłanie strumieniowe obiektów przez przewijanie danych wejściowych i zachowują tylko bieżącą instancję do momentu uzyskania następnej. Nawet korzystając z LINQ, nadal nie musisz przechowywać tego wszystkiego tylko dlatego, że chcesz, aby był filtrowany.

Robert Giesecke
źródło
1

Odpowiadając na ogólne pytanie w tytule, a nie na pytanie szczegółowe:

Jeśli używasz komponentu COM, który zwraca dużo danych (powiedzmy duże tablice 2xN podwójnych) i potrzebny jest tylko mały ułamek, możesz napisać opakowujący komponent COM, który ukrywa pamięć przed .NET i zwraca tylko te dane, które są potrzebne.

To właśnie zrobiłem w mojej głównej aplikacji i znacznie poprawiło to zużycie pamięci.

Peter Mortensen
źródło
0

Zauważyłem, że używanie interfejsów API SetProcessWorkingSetSize lub EmptyWorkingSet do okresowego wymuszania stron pamięci na dysku w długotrwałym procesie może spowodować, że cała dostępna pamięć fizyczna na maszynie skutecznie zniknie, dopóki maszyna nie zostanie ponownie uruchomiona. Mieliśmy bibliotekę .NET DLL załadowaną do procesu natywnego, który używałby interfejsu API EmptyWorkingSet (alternatywy dla używania SetProcessWorkingSetSize) w celu zredukowania zestawu roboczego po wykonaniu zadania wymagającego dużej ilości pamięci. Zauważyłem, że po upływie od 1 dnia do tygodnia komputer wykazywał 99% zużycia pamięci fizycznej w Menedżerze zadań, podczas gdy żaden proces nie zużywał znaczącej ilości pamięci. Wkrótce potem maszyna przestała odpowiadać, co wymagało twardego ponownego uruchomienia. Wspomniane maszyny to ponad 2 tuziny serwerów Windows Server 2008 R2 i 2012 R2 działających zarówno na sprzęcie fizycznym, jak i wirtualnym.

Być może załadowanie kodu .NET do procesu natywnego miało z tym coś wspólnego, ale używaj EmptyWorkingSet (lub SetProcessWorkingSetSize) na własne ryzyko. Być może użyj go tylko raz po pierwszym uruchomieniu aplikacji. Zdecydowałem się wyłączyć kod i pozwolić Garbage Collectorowi samodzielnie zarządzać zużyciem pamięci.

Stuart Welch
źródło