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ę.
źródło
Odpowiedzi:
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.
źródło
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:
List<T>
podobnych typach, które w razie potrzeby podwajają pojemność, ponieważ mogą prowadzić do 50% strat.To prawdopodobnie nie jest wyczerpująca lista, ale tylko kilka pomysłów.
źródło
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.
źródło
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:
źródło
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.
źródło
Nadal istnieją sposoby na ograniczenie prywatnego zestawu roboczego tego prostego programu:
NGEN swoją aplikację. Eliminuje to koszt kompilacji JIT z procesu.
Trenuj swoją aplikację przy użyciu MPGO, zmniejszając zużycie pamięci, a następnie NGEN ją.
źródło
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.
źródło
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.
źródło
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.
źródło