Come ti prepari per le condizioni di memoria insufficiente?


18

Questo può essere facile per i giochi con un ambito ben definito, ma la domanda riguarda i giochi sandbox, in cui il giocatore è autorizzato a creare e costruire qualsiasi cosa .

Possibili tecniche:

  • Utilizzare pool di memoria con limite superiore.
  • Elimina oggetti che non sono più necessari periodicamente.
  • Allocare una quantità aggiuntiva di memoria all'inizio in modo che possa essere successivamente liberata come meccanismo di recupero. Direi circa 2-4 MB.

Ciò è più probabile che accada nelle piattaforme mobili / console in cui la memoria è generalmente limitata a differenza del PC da 16 GB. Suppongo che tu abbia il pieno controllo sull'allocazione / deallocazione della memoria e che non sia coinvolta la garbage collection. Ecco perché lo taggo come C ++.

Nota che non sto parlando dell'Efficace C ++ Item 7 "Preparati a condizioni di memoria insufficiente" , anche se è pertinente, vorrei vedere una risposta più correlata allo sviluppo del gioco, dove di solito hai più controllo su ciò che è accadendo.

Per riassumere la domanda, come ti prepari per le condizioni di memoria insufficiente per i giochi sandbox, quando stai prendendo di mira una piattaforma con console / dispositivo di memoria limitato?


Le allocazioni di memoria non riuscite sono piuttosto rare sui moderni sistemi operativi per PC, perché si scambieranno automaticamente sul disco rigido quando si esaurisce la RAM fisica. Ancora una situazione che dovrebbe essere evitata, perché lo scambio è molto più lento della RAM fisica e influirà notevolmente sulle prestazioni.
Philipp,

@Philipp sì, lo so. Ma la mia domanda è più sui dispositivi a memoria limitata come console e cellulari, penso di averlo menzionato.
concept3d

Questa è una domanda abbastanza ampia (e tipo di sondaggio nel modo in cui è formulata). Puoi restringere un po 'l'ambito per essere più specifico per una singola situazione?
MichaelHouse

@ Byte56 Ho modificato la domanda. Spero che abbia un ambito più definito ora.
concept3d

Risposte:


16

In generale, non gestisci la memoria insufficiente. L'unica opzione sana in un software grande e complesso come un gioco è semplicemente arrestare / affermare / terminare il proprio allocatore di memoria il più presto possibile (specialmente nelle build di debug). Le condizioni di memoria insufficiente vengono testate e gestite in alcuni software di sistema di base o software server in alcuni casi, ma in genere non altrove.

Quando hai un limite di memoria superiore, ti assicuri invece di non aver mai bisogno di più di quella quantità di memoria. Puoi mantenere un numero massimo di NPC consentiti alla volta, ad esempio, e semplicemente smettere di generare nuovi NPC non essenziali una volta colpito quel limite. Per gli NPC essenziali puoi farli sostituire quelli non essenziali o avere un pool / cap separato per gli NPC essenziali che i tuoi progettisti sanno progettare intorno (ad esempio se puoi avere solo 3 NPCsa essenziali, i progettisti non ne inseriranno più di 3 in un'area / pezzo - buoni strumenti aiuteranno i progettisti a farlo correttamente e i test sono ovviamente indispensabili).

Un ottimo sistema di streaming è importante anche in particolare per i giochi sandbox. Non è necessario conservare tutti gli NPC e gli oggetti in memoria. Mentre ti muovi attraverso pezzi del mondo, nuovi pezzi verranno trasmessi in streaming e vecchi pezzi verranno scaricati in streaming. Questi includeranno generalmente NPC e oggetti, nonché il terreno. Progettare e progettare limiti sui limiti degli oggetti deve essere impostato tenendo presente questo sistema, sapendo che al massimo X vecchi pezzi verranno mantenuti in giro e caricati in modo proattivo Y nuovi pezzi verranno caricati, quindi il gioco deve avere spazio per tenere tutto i dati di X + Y + 1 blocchi in memoria.

Alcuni giochi tentano di gestire situazioni di memoria insufficiente con un approccio a due passaggi. Tenendo presente che la maggior parte dei giochi ha molti dati memorizzati nella cache tecnicamente non necessari (diciamo, i vecchi blocchi sopra menzionati) e un'allocazione di memoria potrebbe fare qualcosa del tipo:

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)

Questa è un'ultima misura per far fronte a situazioni impreviste in fase di rilascio, ma durante il debug e il test probabilmente dovresti semplicemente arrestarti immediatamente. Non devi fare affidamento su questo tipo di cose (soprattutto perché il dumping delle cache può avere conseguenze serie sulle prestazioni).

Potresti anche prendere in considerazione il dumping di copie ad alta risoluzione di alcuni dati, ad esempio potresti scaricare i livelli mipmap ad alta risoluzione di trame se stai esaurendo la memoria GPU (o qualsiasi memoria in un'architettura a memoria condivisa). Questo di solito richiede molto lavoro architettonico per farne valere la pena.

Nota che alcuni giochi sandbox molto illimitati possono essere facilmente bloccati, anche su PC (ricorda che le comuni app a 32 bit hanno un limite di 2-3 GB di spazio degli indirizzi anche se hai un PC con 128 GB di RAM; un 64- bit OS e hardware consentono l'esecuzione simultanea di più app a 32 bit ma non può fare nulla per fare in modo che un binario a 32 bit abbia uno spazio di indirizzi maggiore). Alla fine, o hai un mondo di gioco molto flessibile che avrà bisogno di spazio di memoria illimitato per funzionare in ogni caso o hai un mondo molto limitato e controllato che funziona sempre perfettamente nella memoria limitata (o qualcosa nel mezzo).


+1 per questa risposta. Ho scritto due sistemi che funzionano usando lo stile di Sean e pool di memoria discreti ed entrambi hanno finito per funzionare bene in produzione. Il primo era uno spawner che riportava l'output su una curva fino allo spegnimento del limite massimo in modo che il giocatore non avrebbe mai notato una riduzione improvvisa (ritenendo che il throughput totale fosse abbassato da quel margine di sicurezza). Il secondo era legato ai blocchi in quanto un'allocazione non riuscita avrebbe forzato le epurazioni e la riallocazione. Sento che ** un mondo molto limitato e controllato che funziona sempre perfettamente nella memoria limitata ** è vitale per qualsiasi client di lunga durata.
Patrick Hughes il

+1 per menzionare di essere il più aggressivo possibile nella gestione degli errori nelle build di debug. Ricorda che sull'hardware della console di debug a volte hai accesso a più risorse rispetto alla vendita al dettaglio. Potresti voler imitare quelle condizioni sull'hardware di sviluppo assegnando gli oggetti di debug esclusivamente nello spazio degli indirizzi al di sopra di quello che avrebbero i dispositivi di vendita al dettaglio e andrebbero in crash quando lo spazio degli indirizzi equivalente al dettaglio è esaurito.
FlintZA,

5

L'applicazione viene generalmente testata sulla piattaforma di destinazione con gli scenari peggiori e sarai sempre pronto per la piattaforma di destinazione. Idealmente, l'applicazione non dovrebbe mai arrestarsi in modo anomalo, ma a parte l'ottimizzazione per dispositivi specifici, ci sono poche scelte quando si affrontano avvisi di memoria insufficiente.

La migliore pratica è quella di disporre di pool preallocati e il gioco utilizza fin dall'inizio tutta la memoria necessaria. Se il tuo gioco ha un massimo di 100 unità rispetto a un pool per 100 unità e il gioco è fatto. Se 100 unità superano i requisiti mem per un dispositivo di destinazione, è possibile ottimizzare l'unità per utilizzare meno memoria o modificare il design su un massimo di 90 unità. Non ci dovrebbero essere casi in cui puoi costruire cose illimitate, dovrebbe esserci sempre un limite. Sarebbe molto brutto per un gioco sandbox da utilizzare newper ogni istanza perché non puoi mai prevedere l'utilizzo dei mem e un crash è molto peggio di una limitazione.

Inoltre, il design del gioco dovrebbe sempre tenere a mente i dispositivi con il target più basso perché se si basa il design con elementi "illimitati", sarà molto più difficile risolvere i problemi di memoria o modificarlo in seguito.


1

Bene, puoi allocare circa 16 MiB (solo per essere sicuro al 100%) all'avvio o anche in .bssfase di compilazione, e utilizzare un "allocatore sicuro", con una firma simile inline __attribute__((force_inline)) void* alloc(size_t size)( __attribute__((force_inline))è un GCC / mingw-w64attributo che forza l'inserimento di sezioni di codice critico anche se le ottimizzazioni sono disabilitate, anche se dovrebbero essere abilitate per i giochi) invece mallocche tentativi void* result = malloc(size)e se fallisce, rilascia cache, libera la memoria di riserva (o dice ad altro codice di usare la .bsscosa ma che non rientra nell'ambito di questa risposta) e svuota i dati non salvati (salva il mondo sul disco, se usi un concetto di blocchi di tipo Minecraft, chiama qualcosa di simile saveAllModifiedChunks()). Quindi, se malloc(16777216)(assegnando di nuovo questi 16 MiB) fallisce (di nuovo, sostituiscilo con analogico per .bss), termina il gioco e mostraMessageBox(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)o un'alternativa specifica per la piattaforma. Mettere tutto insieme:

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

È possibile utilizzare una soluzione simile con std::set_new_handler(myHandler)dove myHandlerviene void myHandler(void)chiamato quando newfallisce:

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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.