„System.OutOfMemoryException” został zgłoszony, gdy wciąż jest dużo wolnej pamięci

94

To jest mój kod:

int size = 100000000;
double sizeInMegabytes = (size * 8.0) / 1024.0 / 1024.0; //762 mb
double[] randomNumbers = new double[size];

Wyjątek: został zgłoszony wyjątek typu „System.OutOfMemoryException”.

Mam 4 GB pamięci na tym komputerze. 2,5 GB jest wolne, kiedy uruchamiam to działanie, jest wystarczająco dużo miejsca na komputerze, aby obsłużyć 762 MB z 100000000 liczb losowych. Muszę zapisać jak najwięcej liczb losowych, biorąc pod uwagę dostępną pamięć. Kiedy przejdę do produkcji, na pudełku będzie 12 GB i chcę to wykorzystać.

Czy środowisko CLR ogranicza mnie do domyślnej maksymalnej pamięci na początek? i jak poprosić o więcej?

Aktualizacja

Pomyślałem, że podzielenie tego na mniejsze fragmenty i stopniowe zwiększanie wymagań dotyczących pamięci pomogłoby, gdyby problem wynikał z fragmentacji pamięci , ale nie mogę przekroczyć całkowitego rozmiaru ArrayList wynoszącego 256 MB, niezależnie od tego, co robię, dostosowując rozmiar bloku .

private static IRandomGenerator rnd = new MersenneTwister();
private static IDistribution dist = new DiscreteNormalDistribution(1048576);
private static List<double> ndRandomNumbers = new List<double>();

private static void AddNDRandomNumbers(int numberOfRandomNumbers) {
    for (int i = 0; i < numberOfRandomNumbers; i++) {
      ndRandomNumbers.Add(dist.ICDF(rnd.nextUniform()));                
  }
}

Z mojej głównej metody:

int blockSize = 1000000;

while (true) {
  try
  {
    AddNDRandomNumbers(blockSize);                    
  }
  catch (System.OutOfMemoryException ex)
  {
    break;
  }
}            
double arrayTotalSizeInMegabytes = (ndRandomNumbers.Count * 8.0) / 1024.0 / 1024.0;
m3ntat
źródło
6
Zalecałbym zmianę architektury aplikacji, abyś nie musiał zużywać tak dużej ilości pamięci. Co robisz, że potrzebujesz stu milionów liczb naraz w pamięci?
Eric Lippert,
2
nie wyłączyłeś swojego pliku stronicowania ani czegoś takiego, prawda?
jalf
@EricLippert, napotykam to podczas pracy nad problemem P vs. NP ( claymath.org/millenium-problems/p-vs-np-problem ). Czy masz sugestie dotyczące zmniejszenia zużycia pamięci roboczej? (np. serializacja i przechowywanie fragmentów danych na dysku twardym, przy użyciu typu danych C ++ itp.)
devinbost,
@bosit to jest witryna z pytaniami i odpowiedziami. Jeśli masz konkretne pytanie techniczne dotyczące rzeczywistego kodu, opublikuj je jako pytanie.
Eric Lippert
@bostIT, link do problemu P vs. NP w Twoim komentarzu jest już nieaktualny.
RBT

Odpowiedzi:

143

Możesz przeczytać to: „ Brak pamięci” nie odnosi się do pamięci fizycznej ”autorstwa Erica Lipperta.

Krótko mówiąc i bardzo uproszczone, „Brak pamięci” nie oznacza tak naprawdę, że ilość dostępnej pamięci jest zbyt mała. Najczęstszym powodem jest to, że w bieżącej przestrzeni adresowej nie ma ciągłej części pamięci, która byłaby wystarczająco duża, aby obsłużyć żądaną alokację. Jeśli masz 100 bloków, każdy o wielkości 4 MB, to nie pomoże ci, gdy potrzebujesz jednego bloku 5 MB.

Kluczowe punkty:

  • Magazyn danych, który nazywamy „pamięcią procesową”, jest moim zdaniem najlepiej wizualizowany jako ogromny plik na dysku .
  • Pamięć RAM może być postrzegana jedynie jako optymalizacja wydajności
  • Całkowita ilość pamięci wirtualnej, którą zużywa twój program, tak naprawdę nie ma wielkiego znaczenia dla jego wydajności
  • „brak pamięci RAM” rzadko powoduje błąd „braku pamięci”. Zamiast błędu powoduje to słabą wydajność, ponieważ całkowity koszt faktu, że pamięć masowa faktycznie znajduje się na dysku, nagle staje się istotny.
Fredrik Mörk
źródło
„Jeśli masz 100 bloków, każdy z 4 MB duże, że nie pomoże Ci, gdy trzeba jedną 5 MB blok” - myślę, że byłoby lepiej sformułowane z niewielką korektą: „Jeśli masz 100 «»dziura bloki” .
OfirD
31

Sprawdź, czy tworzysz proces 64-bitowy, a nie 32-bitowy, który jest domyślnym trybem kompilacji programu Visual Studio. Aby to zrobić, kliknij prawym przyciskiem myszy swój projekt, Właściwości -> Kompiluj -> docelowa platforma: x64. Jak każdy proces 32-bitowy, aplikacje Visual Studio skompilowane w wersji 32-bitowej mają limit pamięci wirtualnej wynoszący 2 GB.

Procesy 64-bitowe nie mają tego ograniczenia, ponieważ używają wskaźników 64-bitowych, więc ich teoretyczna maksymalna przestrzeń adresowa (rozmiar ich pamięci wirtualnej) wynosi 16 eksabajtów (2 ^ 64). W rzeczywistości Windows x64 ogranicza pamięć wirtualną procesów do 8 TB. Rozwiązaniem problemu z limitem pamięci jest wtedy kompilacja w wersji 64-bitowej.

Jednak rozmiar obiektu w programie Visual Studio jest nadal domyślnie ograniczony do 2 GB. Będziesz mógł utworzyć kilka tablic, których łączny rozmiar będzie większy niż 2 GB, ale domyślnie nie możesz tworzyć tablic większych niż 2 GB. Mamy nadzieję, że jeśli nadal chcesz tworzyć tablice większe niż 2 GB, możesz to zrobić, dodając następujący kod do pliku app.config:

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>
Technologia zmiany biegów
źródło
Uratowałeś mnie. Dziękuję Ci!
Tee Zad Awk
25

Nie masz ciągłego bloku pamięci, aby przydzielić 762 MB, twoja pamięć jest pofragmentowana, a alokator nie może znaleźć wystarczająco dużego otworu, aby przydzielić potrzebną pamięć.

  1. Możesz spróbować pracować z / 3 GB (jak sugerowali inni)
  2. Lub przejdź na 64-bitowy system operacyjny.
  3. Lub zmodyfikuj algorytm, aby nie wymagał dużej ilości pamięci. może przydzielić kilka mniejszych (względnie) porcji pamięci.
Shay Erlichmen
źródło
8

Jak zapewne się domyślasz, problem polega na tym, że próbujesz przydzielić jeden duży, ciągły blok pamięci, który nie działa z powodu fragmentacji pamięci. Gdybym musiał zrobić to, co robisz, zrobiłbym następujące:

int sizeA = 10000,
    sizeB = 10000;
double sizeInMegabytes = (sizeA * sizeB * 8.0) / 1024.0 / 1024.0; //762 mb
double[][] randomNumbers = new double[sizeA][];
for (int i = 0; i < randomNumbers.Length; i++)
{
    randomNumbers[i] = new double[sizeB];
}

Następnie, aby uzyskać określony indeks, którego użyjesz randomNumbers[i / sizeB][i % sizeB].

Inną opcją, jeśli zawsze uzyskujesz dostęp do wartości w kolejności, może być użycie przeciążonego konstruktora do określenia ziarna. W ten sposób otrzymujesz półlosową liczbę (taką jak DateTime.Now.Ticks), przechowuj ją w zmiennej, a następnie kiedykolwiek zaczniesz przeglądać listę, utworzysz nową instancję Random, używając oryginalnego ziarna:

private static int randSeed = (int)DateTime.Now.Ticks;  //Must stay the same unless you want to get different random numbers.
private static Random GetNewRandomIterator()
{
    return new Random(randSeed);
}

Należy zauważyć, że chociaż blog, do którego link znajduje się w odpowiedzi Fredrika Mörka, wskazuje, że problem jest zwykle spowodowany brakiem przestrzeni adresowej , nie zawiera on wielu innych problemów, takich jak ograniczenie rozmiaru obiektu CLR do 2 GB (wspomniane w komentarzu z ShuggyCoUk na tym samym blogu), omawia fragmentację pamięci i nie wspomina o wpływie rozmiaru pliku strony (i o tym, jak można go rozwiązać za pomocą CreateFileMappingfunkcji ).

Ograniczenie 2 GB oznacza, że randomNumbers musi być mniej niż 2 GB. Ponieważ tablice są klasami i mają same narzuty, oznacza to, że tablica doublebędzie musiała być mniejsza niż 2 ^ 31. Nie jestem pewien, o ile mniejsza musiałaby być długość 2 ^ 31, ale narzut macierzy .NET? wskazuje 12-16 bajtów.

Fragmentacja pamięci jest bardzo podobna do fragmentacji dysku twardego. Możesz mieć 2 GB przestrzeni adresowej, ale podczas tworzenia i niszczenia obiektów będą luki między wartościami. Jeśli te luki są zbyt małe dla dużego obiektu i nie można zażądać dodatkowej przestrzeni, otrzymasz plikSystem.OutOfMemoryException. Na przykład, jeśli utworzysz 2 miliony obiektów 1024-bajtowych, używasz 1,9 GB. Jeśli usuniesz każdy obiekt, którego adres nie jest wielokrotnością 3, użyjesz 0,6 GB pamięci, ale zostanie ona rozłożona w przestrzeni adresowej z 2024-bajtowymi otwartymi blokami pomiędzy nimi. Jeśli potrzebujesz stworzyć obiekt o wielkości .2GB, nie będziesz w stanie tego zrobić, ponieważ nie ma bloku wystarczająco dużego, aby go zmieścić i nie można uzyskać dodatkowej przestrzeni (zakładając środowisko 32-bitowe). Możliwe rozwiązania tego problemu to na przykład użycie mniejszych obiektów, zmniejszenie ilości danych przechowywanych w pamięci lub użycie algorytmu zarządzania pamięcią w celu ograniczenia / zapobieżenia fragmentacji pamięci. Należy zauważyć, że jeśli nie tworzysz dużego programu, który używa dużej ilości pamięci, nie będzie to problemem. Również,

Ponieważ większość programów żąda pamięci roboczej z systemu operacyjnego i nie żąda mapowania plików, będą one ograniczone przez pamięć RAM systemu i rozmiar pliku stronicowania. Jak zauważono w komentarzu Néstora Sáncheza (Néstor Sánchez) na blogu, z kodem zarządzanym, takim jak C #, utkniesz w ograniczeniu pamięci RAM / pliku stronicowania i przestrzeni adresowej systemu operacyjnego.


To było o wiele dłużej niż oczekiwano. Mam nadzieję, że to komuś pomoże. Opublikowałem to, ponieważ natknąłem się na System.OutOfMemoryExceptionuruchamianie programu x64 w systemie z 24 GB pamięci RAM, mimo że moja tablica zawierała tylko 2 GB.

Trisped
źródło
„Ponieważ większość programów żąda pamięci roboczej z systemu operacyjnego i nie żąda mapowania plików” - czy miałbyś jakieś zasoby, które dokładniej to wyjaśniają? Kiedy programy wymagają swojego zestawu roboczego do życia w pamięci (i są ograniczone przez rozmiar pamięci RAM), a kiedy programy mogą zapisywać na dysku i są ograniczone przez ciągłą pamięć (jak sugeruje zaakceptowany blog odpowiedzi)
uMdRupert
1
@uMdRupert Minęło trochę czasu, odkąd to zbadałem i napisałem, więc nie mam żadnych dodatkowych zasobów. Czy miałeś okazję przeczytać łącze [CreateFileMapping function] ( msdn.microsoft.com/en-us/library/windows/desktop/aa366537.aspx )? Możesz również poczytać na Paging .
Trisped
5

Odradzałbym opcję uruchamiania systemu Windows / 3GB. Oprócz wszystkiego innego (przesadą jest robienie tego dla jednej źle działającej aplikacji, a prawdopodobnie i tak nie rozwiąże to twojego problemu), może to spowodować dużą niestabilność.

Wiele sterowników systemu Windows nie jest testowanych z tą opcją, więc sporo z nich zakłada, że ​​wskaźniki trybu użytkownika zawsze wskazują niższe 2 GB przestrzeni adresowej. Co oznacza, że ​​mogą strasznie się zepsuć z / 3GB.

Jednak system Windows zwykle ogranicza proces 32-bitowy do 2 GB przestrzeni adresowej. Ale to nie znaczy, że powinieneś spodziewać się możliwości przydzielenia 2 GB!

Przestrzeń adresowa jest już zaśmiecona różnymi rodzajami przydzielonych danych. Jest stos i wszystkie załadowane zestawy, zmienne statyczne i tak dalej. Nie ma gwarancji, że gdziekolwiek będzie dostępne 800 MB ciągłej, nieprzydzielonej pamięci.

Przydzielenie 2 400 MB fragmentów prawdopodobnie wypadłoby lepiej. Lub 4 kawałki po 200 MB. Mniejsze alokacje są znacznie łatwiejsze do znalezienia w pofragmentowanej przestrzeni pamięci.

W każdym razie, jeśli i tak zamierzasz wdrożyć to na maszynie 12 GB, będziesz chciał uruchomić to jako aplikację 64-bitową, która powinna rozwiązać wszystkie problemy.

jalf
źródło
Dzielenie pracy na mniejsze części nie wydaje się pomagać w zobaczeniu mojej aktualizacji powyżej.
m3ntat
4

Zmiana z 32 na 64 bit działała dla mnie - warto spróbować, jeśli używasz 64-bitowego komputera i nie ma potrzeby przenoszenia.

chris
źródło
1

32-bitowe okna mają limit pamięci procesowej 2 GB. Opcja rozruchu / 3 GB, o której wspominali inni, spowoduje, że będzie to 3 GB, a pozostanie tylko 1 GB na jądro systemu operacyjnego. Realistycznie, jeśli chcesz bezproblemowo korzystać z więcej niż 2 GB, wymagany jest 64-bitowy system operacyjny. To również rozwiązuje problem polegający na tym, że chociaż możesz mieć 4 GB fizycznej pamięci RAM, przestrzeń adresowa wymagana dla karty graficznej może sprawić, że spora część tej pamięci stanie się bezużyteczna - zwykle około 500 MB.

redcalx
źródło
1

Zamiast alokować ogromną tablicę, czy możesz spróbować użyć iteratora? Są one wykonywane z opóźnieniem, co oznacza, że ​​wartości są generowane tylko wtedy, gdy są żądane w instrukcji foreach; w ten sposób nie powinno zabraknąć pamięci:

private static IEnumerable<double> MakeRandomNumbers(int numberOfRandomNumbers) 
{
    for (int i = 0; i < numberOfRandomNumbers; i++)
    {
        yield return randomGenerator.GetAnotherRandomNumber();
    }
}


...

// Hooray, we won't run out of memory!
foreach(var number in MakeRandomNumbers(int.MaxValue))
{
    Console.WriteLine(number);
}

Powyższe wygeneruje tyle liczb losowych, ile chcesz, ale generuje je tylko wtedy, gdy są proszone za pomocą instrukcji foreach. W ten sposób nie zabraknie Ci pamięci.

Alternatywnie, jeśli musisz mieć je wszystkie w jednym miejscu, przechowuj je w pliku, a nie w pamięci.

Judah Gabriel Himango
źródło
Ciekawe podejście, ale muszę przechowywać jak najwięcej losowych repozytoriów w dowolnym czasie bezczynności reszty mojej aplikacji, ponieważ ta aplikacja działa w 24-godzinnym zegarze obsługującym wiele regionów geograficznych (wiele przebiegów symulacji Monte Carlo), około 70% dnia maksymalne obciążenie procesora, pozostałe czasy w ciągu dnia Chcę buforować losowe liczby w całej wolnej pamięci. Przechowywanie na dysku jest zbyt wolne i w pewnym sensie niweczy wszelkie korzyści, jakie mógłbym zrobić buforując w tej losowej pamięci podręcznej pamięci.
m3ntat
0

Cóż, mam podobny problem z dużym zestawem danych i próba zmuszenia aplikacji do wykorzystania tak dużej ilości danych nie jest tak naprawdę właściwą opcją. Najlepszą wskazówką, jaką mogę ci dać, jest przetwarzanie danych w małych porcjach, jeśli jest to możliwe. Ponieważ przy tak dużej ilości danych problem prędzej czy później powróci. Ponadto nie możesz znać konfiguracji każdego komputera, na którym będzie działać Twoja aplikacja, więc zawsze istnieje ryzyko, że wyjątek zdarzy się na innym komputerze.

Francis B.
źródło
Właściwie znam konfigurację maszyny, działa ona tylko na jednym serwerze i mogę to napisać dla tych specyfikacji. To jest dla ogromnej symulacji Monte Carlo i staram się zoptymalizować przez buforowanie liczb losowych z góry.
m3ntat
0

Miałem podobny problem, był to skutek StringBuilder.ToString ();

Ricardo Rix
źródło
nie pozwól, aby budowniczy strun stał się tak duży
Ricardo Rix
0

Przekonwertuj swoje rozwiązanie na x64. Jeśli nadal masz problem, nadaj maksymalną długość wszystkim, co powoduje wyjątek, jak poniżej:

 var jsSerializer = new JavaScriptSerializer();
 jsSerializer.MaxJsonLength = Int32.MaxValue;
Samidjo
źródło
0

Jeśli nie potrzebujesz procesu hostingu programu Visual Studio:

Odznacz opcję: Projekt-> Właściwości-> Debuguj-> Włącz proces hostingu programu Visual Studio

A potem buduj.

Jeśli nadal masz problem:

Przejdź do Project-> Properties-> Build Events-> Po-Build Event, wiersz poleceń i wklej:

call "$(DevEnvDir)..\..\vc\vcvarsall.bat" x86
"$(DevEnvDir)..\..\vc\bin\EditBin.exe" "$(TargetPath)"  /LARGEADDRESSAWARE

Teraz skompiluj projekt.

Yasir Arafat
źródło
-2

Zwiększ limit procesów systemu Windows do 3 GB. (przez boot.ini lub menedżera rozruchu Vista)

leppie
źródło
naprawdę? jaka jest domyślna maksymalna pamięć procesu? A jak to zmienić? Jeśli gram w grę lub coś na moim komputerze, może z łatwością wykorzystać 2+ GB z jednego EXE / procesu, nie sądzę, że to jest tutaj problem.
m3ntat
/ 3GB to przesada i może powodować dużą niestabilność, ponieważ wielu kierowców zakłada, że ​​wskaźniki przestrzeni użytkownika zawsze wskazują niższe 2 GB.
jalf
1
m3ntat: Nie, w 32-bitowym systemie Windows pojedynczy proces jest ograniczony do 2 GB. Pozostałe 2 GB przestrzeni adresowej jest używane przez jądro.
jalf