Czy przydzielanie i zwalnianie ogromnej części pamięci przy starcie „czyści pamięć?”

18

Książka Game Coding Complete, czwarta edycja , rozdział 5 ( Inicjalizacja i zamknięcie gry ), sekcja Sprawdzanie pamięci zawiera ten interesujący przykładowy kod:

bool CheckMemory(const DWORDLONG physicalRAMNeeded, const DWORDLONG virtualRAMNeeded)
{
    MEMORYSTATUSEX status; 
    GlobalMemoryStatusEx(&status);
    if (status.ullTotalPhys < physicalRAMNeeded) 
    {
        // you don’t have enough physical memory. Tell the player to go get a 
        // real computer and give this one to his mother. 
        GCC_ERROR("CheckMemory Failure: Not enough physical memory."); 
        return false;
    }
    // Check for enough free memory.
    if (status.ullAvailVirtual < virtualRAMNeeded) 
    {
        // you don’t have enough virtual memory available.
        // Tell the player to shut down the copy of Visual Studio running in the 
        // background, or whatever seems to be sucking the memory dry. 
        GCC_ERROR("CheckMemory Failure: Not enough virtual memory.");
        return false;
    }

    char *buff = GCC_NEW char[virtualRAMNeeded]; 
    if (buff)
    {
        delete[] buff;
    }
    else
    {
        // even though there is enough memory, it isn't available in one
        // block, which can be critical for games that manage their own memory 
        GCC_ERROR("CheckMemory Failure: Not enough contiguous memory."); 
        return false;
    }
}

Rodzi to kilka pytań.

Pierwsza część pyta tylko system operacyjny (Windows), ile fizycznej pamięci RAM jest dostępne. Ciekawą częścią jest druga, która przydziela ogromną część pamięci i od razu ją uwalnia:

char *buff = GCC_NEW char[virtualRAMNeeded]; 
if (buff)
{
    delete[] buff;
}

Autor wyjaśnia dalej:

... ta funkcja przydziela i natychmiast uwalnia ogromny blok pamięci. Powoduje to, że system Windows usuwa wszelkie śmieci nagromadzone w menedżerze pamięci i dwukrotnie sprawdza, czy można przydzielić ciągły blok tak duży, jak potrzebujesz. Jeśli połączenie się powiedzie, w zasadzie uruchamiasz odpowiednik maszyny Zamboni w pamięci systemu, przygotowując ją do uderzenia w lód ...

Ale mam co do tego zastrzeżenia.

„Czyścisz śmieci, które nagromadziły się w menedżerze pamięci?” Naprawdę? Jeśli gra właśnie się rozpoczęła, czy nie powinno być śmieci?

„Upewnij się, że możesz przydzielić ciągły blok?” W bardzo szczególnym przypadku, w którym zamierzasz samodzielnie zarządzać pamięcią, miałoby to jakiś sens, ale mimo to, jeśli przydzielisz dużo pamięci bezpośrednio od nietoperza, praktycznie uniemożliwisz uruchomienie innej aplikacji system, gdy twój jest włączony.

Ponadto, czy nie zmusi to systemu operacyjnego do zatwierdzenia całej pamięci, aw konsekwencji spowoduje wyrzucenie dużej ilości pamięci na miejsce na dysku wymiany, co spowalnia uruchomienie aplikacji?

Czy to naprawdę dobra praktyka?

glampert
źródło
3
Większość współczesnych systemów operacyjnych nic nie zrobi, gdy aplikacja przydzieli duży obszar pamięci, wykorzysta przydział optymistyczny i właściwie nic nie zrobi, dopóki nie
zapełnisz
Czy usunięcie z lasu długiego pasa ziemi, wykrojenie radia, słuchawek itp. Powoduje, że samoloty lądują i dostarczają zapasy?
Dan Neely
10
Game Coding Complete zawiera wiele bzdur w połączeniu z dużą ilością niezrozumiałych C ++ i C, które wyglądają trochę jak C ++ (pokazane również w tym przykładzie, sprawdzając wynik operator newdla nullptr), jeśli pozwolisz mi powiedzieć. Najlepszą rzeczą, jaką możesz zrobić z tą książką, jest rozpalenie komina. Przydzielanie i zwalnianie dużego bloku pamięci oczywiście nie „oczyszcza” pamięci.
Damon
@Damon, nie sprawdziłem, ale podejrzewam, że przynajmniej przeciążyli globalnego newoperatora, aby zwrócił wartość zero zamiast rzucać bad_alloc. Jeśli nie, to tak, ten kod jest jeszcze bardziej bezsensowny: P
glampert
1
@glampert: Nawet zakładając, że tak operator deletejest , należy zaakceptować nullptri potraktować to jako brak możliwości. Każde globalne przeciążenie, które tego nie robi, jest zepsute. Co oznacza, że ​​jest to nonsensowne w obu przypadkach. Podobnie jak założenie, że przydzielenie ogromnego bloku pamięci i zwolnienie go „magicznie” zrobi coś dobrego. W najlepszym wypadku nie zaszkodzi (najprawdopodobniej, ponieważ strony nie są nawet dotykane ... w przeciwnym razie może zamienić niektóre strony z zestawu roboczego, które trzeba będzie ponownie załadować).
Damon

Odpowiedzi:

12

Jedną z rzeczy, które można zrobić wstępnie przydzielając dużą część pamięci , jeśli na komputerze jest mało pamięci RAM, może być wymuszenie zwolnienia przez system operacyjny dodatkowej przestrzeni przez zamianę części pamięci innych programów na dysk.

Ponieważ taka zamiana jest na ogół bardzo powolną operacją, która praktycznie zawiesza Twój program podczas jego trwania, pewną zaletą może być zapewnienie, że jeśli tak się stanie, nastąpi to przed rozpoczęciem gry, a nie w trakcie rozgrywki.

To powiedziawszy, zmuszanie takiej zamiany na dysk nie jest w 100% niezawodne:

  • W wielu implementacjach pamięci wirtualnej samo przydzielenie pamięci tak naprawdę nie powoduje wymiany; dzieje się tak tylko wtedy, gdy po raz pierwszy uzyskujesz dostęp do każdej przydzielonej strony pamięci. (Jest to z pewnością prawdziwe w nowoczesnych wersjach systemu Linux, z włączoną funkcją nadpisywania pamięci, ponieważ jest ona domyślnie; nie wiem na pewno, w jaki sposób obsługują ją różne wersje systemu Windows).

    Tak więc, jeśli naprawdę chcesz, aby ten kod „wyczyścił pamięć”, prawdopodobnie powinieneś dodać memset()wywołanie lub odpowiednik, aby faktycznie zapisać dane do każdej części tablicy (lub przynajmniej do jej pierwszych physicalRAMNeededbajtów) przed zwolnieniem.

  • Ponadto, zmuszając OS aby zamienić innych programów z pamięci RAM jak to nie znaczy, że będą one pozostać z niego. Windows to system wielozadaniowy, a gdy tylko jeden z tych zamienionych programów chce ponownie uruchomić i użyć swoich zamienionych danych, zostanie ponownie włączony, potencjalnie ponownie zamrażając grę.

    Dlatego ta sztuczka jest na ogół użyteczna tylko wtedy, gdy pamięć RAM jest pochłaniana przez duże porcje danych, do których nie ma aktywnego dostępu (takie jak duże dokumenty pozostawione otwarte w tle w niektórych programach do edycji). Przeglądarki internetowe są pod tym względem szczególnie problematyczne, ponieważ nawet jeśli nie używasz strony internetowej otwartej w zakładce w tle, strona może nadal mieć uruchomione skrypty, które zmuszają ją do przechowywania w pamięci RAM.

    (Nie sposoby, aby program mógł właściwie powiedzieć OS zablokować część swojej przestrzeni pamięci do pamięci RAM i zapobiec jego wyswapowany, ale te wymagają zwykle podwyższonymi uprawnieniami. W każdym razie, kod zostanie zaksięgowana nie wydaje się być przy użyciu takich metod).

Zasadniczo ta sztuczka wydaje się przydatna tylko w tej stosunkowo wąskiej sytuacji na granicy, w której użytkownik miałby wystarczającą ilość pamięci RAM, aby uruchomić grę (i każde inne oprogramowanie aktywnie działające w tle) bez zamiany, ale ta pamięć RAM jest obecnie wypełniona nieaktywnymi i nieużywanymi otwartymi plikami lub inne dane, które można i należy zamienić na dysk podczas działania gry. W takich sytuacjach może sprawić, że Twoja gra będzie wydawać się płynniejsza i bardziej responsywna, zastępując sporadyczne małe operacje wymiany podczas gry na jedną podczas uruchamiania.

Ilmari Karonen
źródło
1
Z mojej ograniczonej wiedzy na temat zarządzania pamięcią niskiego poziomu mogę stwierdzić, że decyzja o zamianie konkretnej strony jest o wiele bardziej złożona i bierze pod uwagę o wiele więcej czynników niż po prostu wymagana ilość pamięci i dostępna pamięć . Uproszczenie tego i ogólnie poleganie na przypuszczeniach nie jest zbyt mądre.
Panda Pajama
3
Myślę, że ważne jest, aby pamiętać, że wszystkie te przypuszczenia mogą działać, a nie działać lub wydawać się działać, ale z zupełnie niezwiązanych powodów. O ile mi wiadomo, żadne z zachowań wskazanych w tej odpowiedzi nie jest udokumentowane i poleganie na nich byłoby nierozsądne. Aktualizacja systemu operacyjnego, zmiana ustawień, zmiana kompilatora lub prawie wszystko może sprawić, że przestanie to działać, a nawet może mieć negatywny wpływ na wydajność.
Panda Pajama
26

Nie wiem, ile lat ma ten zapis, ale powiedziałbym, że jest dość stary. W nowoczesnym systemie Windows (XP i nowszych, ale szczególnie w wersjach 64-bitowych) powiedziałbym, że zrobienie czegoś takiego będzie miało niewielki lub negatywny wpływ na uruchomienie twojej gry.

Przede wszystkim pamiętaj, że przestrzeń adresowa jest wirtualizowana dla każdego procesu. Jeśli chodzi o proces, masz całą przestrzeń adresową dla siebie i zawsze otrzymasz ciągłą (wirtualną) pamięć, niezależnie od tego, czy ta pamięć jest fizycznie ciągła, czy nie.

W rzeczywistości to, czy pamięć jest ciągła w pamięci fizycznej, nie jest czymś, nad czym masz kontrolę. System operacyjny wybierze sposób alokacji fizycznych bloków według własnego uznania i nic, co zrobisz na poziomie aplikacji, nie może mieć wpływu na korzyści (jeśli w ogóle) wynikające z posiadania ciągłej pamięci fizycznej.

Musisz dbać o fragmentację pamięci wirtualnej, jeśli przez bardzo długi czas przydzielasz i zwalniasz pamięć o różnych rozmiarach. Jest to oczywiście znacznie mniej istotne w przypadku 64-bitowej przestrzeni adresowej. Gry zwykle nie działają przez miesiące, więc najprawdopodobniej nie będziesz musiał się tym przejmować, chyba że mówimy o długo działających serwerach gier.

Nawet jeśli przydzielasz dużo pamięci o bardzo różnych rozmiarach na maszynie 32-bitowej, nowoczesne przydziały pamięci są całkiem dobre, więc jest mało prawdopodobne, że wystąpią problemy z fragmentacją pamięci wirtualnej, chyba że aktywnie ich szukasz

Szczerze mówiąc, nie wiem, o co chodzi z tym pisarzem. Nawet jeśli przestrzeń adresowa była współdzielona i otrzymałeś ogromny ciągły blok w pamięci fizycznej, uwalniając ją, mówisz, że już cię to nie obchodzi. Istnieją inne programy działające w tle, które dokonują własnych alokacji, więc nawet jeśli twój wielki malloc odniesie sukces, nikt nie zagwarantuje, że następny również odniesie sukces.

Myślę, że to przypadek „zadziałało z jakiegoś powodu, którego tak naprawdę nie rozumiem, więc wymyśliłem coś, żeby to wyjaśnić”.

Panda Piżama
źródło
10
Pamięć RAM nie przypomina obrotowego dysku twardego, w którym ciągłe bloki są w jakikolwiek sposób „lepsze” niż nie. To nie jest prawda. ciągły dostęp do pamięci RAM jest o rząd wielkości szybszy niż dostęp losowy. Układy pamięci RAM są podzielone na sekcje, dla których istnieje pewien czas oczekiwania na zmianę, a co ważniejsze, brak pamięci podręcznej L1 i L2 drastycznie wpłynie na wydajność, gdy pamięć zostanie rozproszona.
CaptainCodeman
@CaptainCodeman: Muszę zaakceptować, że wykracza to poza moje pole wiedzy, ale w żaden sposób, jako programista aplikacji dla systemu Windows, nie masz kontroli nad tym, czy twoja pamięć jest fizycznie ciągła, czy nie. Pytanie o ogromny blok pamięci niewiele się zmieni.
Panda Pajama
@CaptainCodeman faktycznie ma ciągły blok wirtualny w ciągłym mod 2 ^ N blok w pamięci RAM przyspieszy go z powodu alokacji linii pamięci podręcznej
maniak ratchet
8
@CaptainCodeman Tak, sekwencyjny dostęp do pamięci DRAM jest szybszy, ale mówimy o wirtualnym> mapowaniu fizycznym, które odbywa się na stronach 4 KB (lub więcej), więc to, czy mapowanie jest sekwencyjne, czy nie, nie powinno mieć większego wpływu na pamięć odczyt prędkości. Ponadto pobieranie danych z pamięci podręcznej i tego typu operacje działają w wirtualnej przestrzeni adresowej, a nie fizycznie.
Nathan Reed
1
@NathanReed Wystarczająco i dziękuję za wyjaśnienie. Właśnie wyraziłem, że prędkość dostępu do pamięci RAM nie jest całkowicie jednolita w przestrzeni pamięci, jak sugerowano.
CaptainCodeman