Como você se prepara para condições de falta de memória?

18

Isso pode ser fácil para jogos com escopo bem definido, mas a questão é sobre jogos em área restrita, onde o jogador pode criar e construir qualquer coisa .

Possíveis técnicas:

  • Use conjuntos de memória com limite superior.
  • Exclua objetos que não são mais necessários periodicamente.
  • Aloque uma quantidade extra de memória no início para que possa ser liberada posteriormente como um mecanismo de recuperação. Eu diria cerca de 2-4 MBs.

É mais provável que isso aconteça em plataformas móveis / console, onde a memória geralmente é limitada, diferentemente do seu PC de 16 GB. Suponho que você tenha controle total sobre a alocação / desalocação de memória e nenhuma coleta de lixo envolvida. É por isso que estou marcando isso como C ++.

Observe que não estou falando do item 7 do C ++ eficaz "Esteja preparado para condições de falta de memória" , mesmo que seja relevante, gostaria de ver uma resposta mais relacionada ao desenvolvimento de jogos, onde você geralmente tem mais controle sobre o que é acontecendo.

Para resumir a pergunta, como você se prepara para condições de falta de memória para jogos em sandbox, quando está direcionando uma plataforma com console / dispositivo de memória limitado?

concept3d
fonte
Alocações de memória com falha são muito raras nos sistemas operacionais de PC modernos, porque elas serão automaticamente trocadas para o disco rígido quando a RAM física estiver acabando. Ainda é uma situação que deve ser evitada, porque a troca é muito mais lenta que a RAM física e afetará gravemente o desempenho.
Philipp
@ Philip sim, eu sei. Mas minha pergunta é mais para dispositivos com memória limitada, como consoles e celulares, acho que mencionei isso.
precisa
Esta é uma pergunta bastante ampla (e meio que uma enquete do jeito que está redigida). Você pode restringir um pouco o escopo para ser mais específico a uma única situação?
MichaelHouse
@ Byte56 eu editei a pergunta. Espero que tenha um escopo mais definido agora.
precisa

Respostas:

16

Geralmente, você não lida com falta de memória. A única opção sensata em software tão grande e complexo quanto um jogo é simplesmente travar / afirmar / encerrar no seu alocador de memória o mais rápido possível (especialmente em compilações de depuração). As condições de falta de memória são testadas e manipuladas em alguns softwares do sistema principal ou em servidores em alguns casos, mas geralmente não em outros lugares.

Quando você tem um limite de memória superior, apenas garante que nunca precisará de mais do que essa quantidade de memória. Você pode manter um número máximo de NPCs permitidos por vez, por exemplo, e simplesmente parar de gerar novos NPCs não essenciais assim que o limite for atingido. Para NPCs essenciais, você pode substituí-los por outros não essenciais ou ter um pool / limite separado para NPCs essenciais que seus designers sabem projetar (por exemplo, se você pode ter apenas 3 NPCsa essenciais, os designers não colocarão mais do que 3 em uma área / parte - boas ferramentas ajudarão os designers a fazer isso corretamente e o teste é essencial, é claro).

Um sistema de streaming realmente bom também é importante, especialmente para jogos em sandbox. Você não precisa manter todos os NPCs e itens na memória. À medida que você avança por partes do mundo, novas partes serão inseridas e partes antigas serão exibidas. Isso geralmente inclui NPCs e itens, além de terrenos. Os limites de projeto e engenharia nos limites de itens precisam ser definidos com esse sistema em mente, sabendo que no máximo X trechos antigos serão mantidos ao redor e carregados de forma proativa Y serão carregados novos trechos, para que o jogo precise ter espaço para manter tudo os dados de pedaços X + Y + 1 na memória.

Alguns jogos tentam lidar com situações de falta de memória com uma abordagem de duas passagens. Lembre-se de que a maioria dos jogos possui muitos dados em cache tecnicamente desnecessários (por exemplo, os blocos antigos mencionados acima) e uma alocação de memória pode fazer algo como:

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)

Esta é uma medida de última parada para lidar com situações inesperadas no lançamento, mas durante a depuração e teste, você provavelmente deve travar imediatamente. Você não precisa depender desse tipo de coisa (especialmente porque o descarte dos caches pode ter sérias conseqüências de desempenho).

Você também pode considerar despejar cópias de alta resolução de alguns dados, por exemplo, pode despejar os níveis de texturas mipmap de maior resolução se estiver com pouca memória GPU (ou qualquer memória em uma arquitetura de memória compartilhada). Isso geralmente requer muito trabalho arquitetônico para fazer valer a pena, no entanto.

Observe que alguns jogos sandbox muito ilimitados podem ser facilmente travados, mesmo no PC (lembre-se de que os aplicativos comuns de 32 bits têm um limite de 2-3 GB de espaço de endereço, mesmo se você tiver um PC com 128 GB de RAM; O SO e o hardware de bit permite que mais aplicativos de 32 bits sejam executados simultaneamente, mas não podem fazer nada para fazer com que um binário de 32 bits tenha um espaço de endereço maior). No final, você tem um mundo de jogo muito flexível que precisará de espaço de memória ilimitado para rodar em todos os casos ou um mundo muito limitado e controlado que sempre funciona perfeitamente na memória limitada (ou algo no meio).

Sean Middleditch
fonte
+1 para esta resposta. Escrevi dois sistemas que funcionam usando o estilo de Sean e conjuntos de memória discretos e ambos acabaram funcionando bem na produção. O primeiro foi um gerador que reverteu a saída em uma curva até o limite máximo de interrupção, para que o jogador nunca notasse uma redução repentina (embora o rendimento total tenha sido reduzido por essa margem de segurança). O segundo estava relacionado aos pedaços, pois uma falha na alocação forçaria expurgos e realocações. Eu sinto que ** um mundo muito limitado e controlado que sempre funciona perfeitamente em memória limitada ** é vital para qualquer cliente de longa duração.
Patrick Hughes
+1 por mencionar ser o mais agressivo possível com o tratamento de erros em compilações de depuração. Lembre-se de que, no hardware do console de depuração, às vezes você tem acesso a mais recursos do que o varejo. Você pode imitar essas condições no hardware do desenvolvedor alocando objetos de depuração exclusivamente no espaço de endereço acima do que os dispositivos de varejo teriam e travando quando o espaço de endereço equivalente ao varejo é usado.
FlintZA
5

O aplicativo geralmente é testado na plataforma de destino com os piores cenários e você sempre estará preparado para a plataforma de destino. Idealmente, o aplicativo nunca deve falhar, mas, além da otimização para dispositivos específicos, há poucas opções quando você recebe um aviso de pouca memória.

A melhor prática é ter pools pré-alocados e o jogo usa desde o início toda a memória necessária. Se o seu jogo tiver no máximo 100 unidades, você terá um pool para 100 unidades e pronto. Se 100 unidades excederem os requisitos de memória para um dispositivo de destino, você poderá otimizar a unidade para usar menos memória ou alterar o design para no máximo 90 unidades. Não deve haver caso em que você possa construir coisas ilimitadas, sempre deve haver um limite. Seria muito ruim para um jogo de sandbox usar newpara cada instância, porque você nunca pode prever o uso de mem e uma falha é muito pior do que uma limitação.

Além disso, o design do jogo deve sempre ter em mente os dispositivos mais baixos, porque se você basear seu design com itens "ilimitados", será muito mais difícil resolver os problemas de memória ou alterar o design posteriormente.

Raxvan
fonte
1

Bem, você pode alocar cerca de 16 MiB (apenas para ter 100% de certeza) na inicialização ou mesmo em .bsstempo de compilação e usar um "alocador seguro", com uma assinatura como inline __attribute__((force_inline)) void* alloc(size_t size)( __attribute__((force_inline))é um mingw-w64atributo / GCC que força o alinhamento de seções críticas de código mesmo que as otimizações estejam desativadas, mesmo que devam estar ativadas para jogos), em vez de malloctentar, void* result = malloc(size)e se falhar, elimine caches, libere a memória sobressalente (ou diga a outro código para usar a .bsscoisa, mas isso está fora do escopo desta resposta) e libere dados não salvos (salve o mundo no disco, se você usar um conceito de blocos do tipo Minecraft, chame algo assim saveAllModifiedChunks()). Então, se malloc(16777216)(alocar esses 16 MiB novamente) falhar (novamente, substitua por analógico por .bss), encerre o jogo e mostreMessageBox(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)ou uma alternativa específica da plataforma. Juntando tudo:

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

Você pode usar uma solução semelhante com std::set_new_handler(myHandler)onde myHandleré void myHandler(void)chamado quando newfalha:

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
fonte