Jak przygotować się na warunki braku pamięci?

18

Może to być łatwe w przypadku gier o ściśle określonym zakresie, ale pytanie dotyczy gier typu sandbox, w których gracz może tworzyć i budować wszystko .

Możliwe techniki:

  • Użyj pul pamięci z górnym limitem.
  • Usuń obiekty, które nie są już potrzebne okresowo.
  • Przydziel dodatkową pamięć na początku, aby można ją było później zwolnić jako mechanizm odzyskiwania. Powiedziałbym, że około 2-4 MB.

Jest to bardziej prawdopodobne na platformach mobilnych / konsolowych, w których pamięć jest zwykle ograniczona w przeciwieństwie do komputera 16 GB. Zakładam, że masz pełną kontrolę nad przydzielaniem / zwalnianiem pamięci i nie wiąże się to z odśmiecaniem pamięci. Dlatego oznaczam to jako C ++.

Pamiętaj, że nie mówię o Effective C ++ pozycja 7 „Bądź przygotowany na warunki braku pamięci” , mimo że jest to istotne, chciałbym zobaczyć odpowiedź bardziej związaną z tworzeniem gier, w której zwykle masz większą kontrolę nad tym, co jest wydarzenie.

Podsumowując pytanie, w jaki sposób przygotowujesz się na warunki braku pamięci w grach piaskownicy, gdy celujesz w platformę z ograniczoną pamięcią konsoli / urządzenia mobilnego?

concept3d
źródło
Nieudane przydziały pamięci są dość rzadkie w nowoczesnych systemach operacyjnych PC, ponieważ automatycznie zamieniają się na dysk twardy, gdy kończy się fizyczna pamięć RAM. Nadal jest to sytuacja, której należy unikać, ponieważ zamiana jest znacznie wolniejsza niż fizyczna pamięć RAM i poważnie wpłynie na wydajność.
Filipiny,
@Filipp tak wiem. Ale moje pytanie dotyczy raczej urządzeń z ograniczoną pamięcią, takich jak konsole i telefony komórkowe. Myślę, że o tym wspominałem.
concept3d
To dość szerokie pytanie (i rodzaj ankiety w jego brzmieniu). Czy możesz zawęzić nieco zakres, aby być bardziej konkretnym dla pojedynczej sytuacji?
MichaelHouse
@ Byte56 Zredagowałem pytanie. Mam nadzieję, że ma teraz bardziej zdefiniowany zakres.
concept3d

Odpowiedzi:

16

Zasadniczo nie radzisz sobie z brakiem pamięci. Jedyną rozsądną opcją w oprogramowaniu tak dużym i złożonym, jak gra, jest jak najszybsze zawieszenie / potwierdzenie / zakończenie pracy w alokatorze pamięci (szczególnie w kompilacjach debugowania). Warunki braku pamięci są testowane i obsługiwane w niektórych podstawowych programach systemowych lub oprogramowaniu serwerowym w niektórych przypadkach, ale zwykle nie w innym miejscu.

Gdy masz górny limit pamięci, po prostu upewnij się, że nigdy nie potrzebujesz więcej niż tyle pamięci. Na przykład możesz zatrzymać maksymalną liczbę dozwolonych NPC na raz i po prostu przestać pojawiać się nowych nieistotnych NPC po osiągnięciu tego limitu. W przypadku niezbędnych NPC możesz albo zastąpić nieistotnych, albo mieć osobną pulę / limit dla niezbędnych NPC, o których projektanci wiedzą, że projektują wokół (np. Jeśli możesz mieć tylko 3 niezbędne NPCsa, projektanci nie umieszczą więcej niż 3 w obszar / fragment - dobre narzędzia pomogą projektantom zrobić to poprawnie, a testowanie jest oczywiście niezbędne).

Naprawdę dobry system przesyłania strumieniowego jest również ważny, szczególnie w grach z piaskownicą. Nie musisz przechowywać w pamięci wszystkich NPC i przedmiotów. Podczas poruszania się po częściach świata nowe fragmenty będą przesyłane strumieniowo, a stare fragmenty przesyłane strumieniowo. Będą to na ogół NPC i przedmioty, a także teren. Należy wziąć pod uwagę ograniczenia projektowe i inżynierskie dotyczące limitów przedmiotów, mając na uwadze, że co najwyżej X starych kawałków zostanie zachowanych, a proaktywnie załadowane Y nowych kawałków, więc gra musi mieć miejsce na wszystko dane fragmentów X + Y + 1 w pamięci.

Niektóre gry próbują poradzić sobie z sytuacjami braku pamięci przy podejściu dwuprzebiegowym. Pamiętając, że większość gier ma wiele zbędnych technicznie zbuforowanych danych (powiedzmy, stare fragmenty wspomniane powyżej), a przydział pamięci może zrobić coś takiego:

allocate(bytes):
  if can_allocate(bytes):
    return internal_allocate(bytes)
  else:
    warning(LOW_MEMORY)
    tell_systems_to_dump_caches()

    if can_allocate(bytes):
      return internal_allocate(bytes)
    else:
      fatal_error(OUT_OF_MEMORY)

Jest to ostatni krok do rozwiązania nieoczekiwanych sytuacji w wydaniu, ale podczas debugowania i testowania prawdopodobnie powinieneś natychmiast po prostu ulec awarii. Nie chcesz polegać na tego rodzaju rzeczach (szczególnie dlatego, że zrzucanie pamięci podręcznych może mieć poważne konsekwencje dla wydajności).

Możesz również rozważyć zrzucenie kopii niektórych danych w wysokiej rozdzielczości, na przykład możesz zrzucić poziomy tekstur mipmap w wyższej rozdzielczości, jeśli masz mało pamięci GPU (lub dowolnej pamięci w architekturze pamięci współdzielonej). Jednak zazwyczaj wymaga to dużo pracy architektonicznej.

Pamiętaj, że niektóre bardzo nieograniczone gry z piaskownicą można dość łatwo po prostu rozbić, nawet na PC (pamiętaj, że popularne 32-bitowe aplikacje mają limit 2-3 GB przestrzeni adresowej, nawet jeśli masz komputer z 128 GB pamięci RAM; 64- bit OS i sprzęt pozwala na jednoczesne działanie większej liczby 32-bitowych aplikacji, ale nie można nic zrobić, aby 32-bitowy plik binarny miał większą przestrzeń adresową). W końcu albo masz bardzo elastyczny świat gry, który będzie wymagał nieograniczonej przestrzeni pamięci do działania w każdym przypadku, albo masz bardzo ograniczony i kontrolowany świat, który zawsze działa idealnie w ograniczonej pamięci (lub coś gdzieś pomiędzy).

Sean Middleditch
źródło
+1 za tę odpowiedź. Napisałem dwa systemy, które działają w stylu Seana i dyskretnych pulach pamięci, i oba działały dobrze w produkcji. Pierwszym był spawn, który wycofał dane wyjściowe na krzywej do maksymalnego odcięcia limitu, aby gracz nigdy nie zauważył nagłego zmniejszenia (sądząc, że całkowity przepływ został obniżony o ten margines bezpieczeństwa). Drugi był związany z częściami, ponieważ nieudany przydział wymusiłby czystki i realokację. Uważam, że ** bardzo ograniczony i kontrolowany świat, który zawsze działa idealnie w ograniczonej pamięci ** jest niezbędny dla każdego klienta działającego długo.
Patrick Hughes,
+1 za wzmiankę o byciu tak agresywnym w obsłudze błędów w kompilacjach debugowania, jak to możliwe. Pamiętaj, że na sprzęcie do debugowania czasami masz dostęp do większej ilości zasobów niż do sprzedaży detalicznej. Możesz naśladować te warunki na sprzęcie programistycznym, przydzielając obiekty debugowania wyłącznie w przestrzeni adresowej powyżej tego, co miałyby urządzenia detaliczne, i zawieszając się, gdy przestrzeń adresowa odpowiadająca detalowi zostanie zużyta.
FlintZA,
5

Aplikacja jest zwykle testowana na platformie docelowej z najgorszymi scenariuszami i zawsze będziesz przygotowany na platformę, na którą jesteś kierowany. Idealnie byłoby, gdyby aplikacja nigdy nie ulegała awarii, ale poza optymalizacją dla konkretnych urządzeń, wybór ostrzeżenia o niskim poziomie pamięci jest niewielki.

Najlepszą praktyką jest posiadanie wstępnie przydzielonych pul, a gra od samego początku wykorzystuje całą potrzebną pamięć. Jeśli twoja gra ma maksymalnie 100 jednostek, to masz pulę na 100 jednostek i to wszystko. Jeśli 100 jednostek przekroczy wymagania pamięci dla jednego docelowego urządzenia, możesz zoptymalizować jednostkę, aby zużywała mniej pamięci lub zmienić projekt na maksymalnie 90 jednostek. Nie powinno być przypadku, w którym można budować nieograniczoną liczbę rzeczy, zawsze powinien istnieć limit. Używanie gry piaskownicy newdla każdej instancji byłoby bardzo złe, ponieważ nigdy nie można przewidzieć użycia pamięci, a awaria jest znacznie gorsza niż ograniczenie.

Również projekt gry powinien zawsze uwzględniać urządzenia o najniższym poziomie docelowym, ponieważ jeśli oprzesz swój projekt na „nieograniczonych” rzeczach, znacznie trudniej będzie rozwiązać problemy z pamięcią lub zmienić projekt później.

Raxvan
źródło
1

Cóż, możesz przydzielić około 16 MiB (żeby być w 100% pewnym) przy starcie lub nawet w .bssczasie kompilacji, i użyć „bezpiecznego alokatora” z podobną sygnaturą inline __attribute__((force_inline)) void* alloc(size_t size)( __attribute__((force_inline))to GCC / mingw-w64atrybut, który wymusza wstawianie krytycznych sekcji kodu nawet jeśli optymalizacje są wyłączone, nawet jeśli powinny być włączone dla gier), zamiast malloctego próbuje, void* result = malloc(size)a jeśli się nie powiedzie, upuść pamięć podręczną, zwolnij wolną pamięć (lub powiedz innemu kodowi, aby użył tej .bssrzeczy, ale to nie wchodzi w zakres tej odpowiedzi) i opróżnij niezapisane dane (uratuj świat na dysku, jeśli używasz koncepcji kawałków w stylu Minecraft, zadzwoń jakoś saveAllModifiedChunks()). Następnie, jeśli malloc(16777216)(ponowne przydzielenie tych 16 MiB) nie powiedzie się (ponownie, zamień na analogowy dla .bss), zakończ grę i pokażMessageBox(NULL, "*game name* couldn't continue because of lack of free memory, but your world was safely saved. Try closing background applications and restarting the game", "*Game name*: out of memory", MB_ICONERROR)lub alternatywa dla konkretnej platformy. Kładąc wszystko razem:

__attribute__((force_inline)) void* alloc(size_t size) {
    void* result = malloc(size); // Attempt to allocate normally
    if (!result) { // If the allocation failed...
        if (!reserveMemory) std::_Exit(); // If alloc() was called from forceFullSave() or reportOutOfMemory() and we again can't allocate, just quit, something is stealing all our memory. If we used the .bss approach, this wouldn't've been necessary.
        free(reserveMemory); // Global variable, pointer to the reserve 16 MiB allocated on startup
        forceFullSave(); // Saves the game
        reportOutOfMemory(); // Platform specific error message box code
        std::_Exit(); // Close silently
    } else return result;
}

Można użyć podobnego rozwiązania z std::set_new_handler(myHandler)którym myHandlerjest void myHandler(void), że jest wywoływana gdy newnie powiedzie się:

void newerrhandler() {
    if (!reserveMemory) std::_Exit(); // If new was called from forceFullSave() or reportOutOfMemory() and we again can't allocate, just quit, something is stealing all our memory. If we used the .bss approach, this wouldn't've been necessary.
    free(reserveMemory); // Global variable, pointer to the reserve 16 MiB allocated on startup
    forceFullSave(); // Saves the game
    reportOutOfMemory(); // Platform specific error message box code
    std::_Exit(); // Close silently
}

// In main ()...
std::set_new_handler(newerrhandler);
Vladislav Toncharov
źródło