Padrões de alocação de memória usados ​​no desenvolvimento de jogos

20

Eu tenho pesquisado a criação de meus próprios métodos de alocador (que suportarão coisas como pool de memória e criação de perfil), no entanto, à medida que continuo minha pesquisa, procurei como isso foi feito no desenvolvimento de jogos.

Que técnica de alocação de memória eu poderia usar e por que é uma boa técnica?

chadb
fonte
1
você realmente precisa? é apenas uma das coisas mais complicadas que uma equipe pode implementar, se é que pode implementá-la.
Ali1S232
4
É um campo de interesse para mim, então eu gostaria de aprender sobre ele e implementá-lo.
chadb 18/03/12
Devo dizer que o assunto é realmente interessante ... Há casos em que podem significar allot, mas em você jogo médio PC Eu preferiria se preocupar com o jogo real ...
rioki
Você pesquisou o código-fonte da moderna biblioteca padrão malloc e é gratuita ou nova e exclui? Eu pergunto porque parece que isso forneceria uma base muito útil para comparar qualquer estratégia de alocação alternativa com algoritmo ou praticamente. Parece que isso também forneceria algumas informações reais sobre o que você entrará.
Louis Langholtz

Respostas:

25

A arquitetura do Game Engine tem algumas informações sobre este tópico. O básico é que você precisa fazer algumas análises para entender quais são os requisitos de sua memória por nível / quadro / etc. são como, mas existem alguns padrões que o autor menciona ter visto várias vezes:

  • Alocadores baseados em pilha: alocam um grande segmento de memória uma vez e, em seguida, alocam ponteiros dentro desse bloco de memória em resposta a solicitações de outras partes do jogo. Isso é útil para evitar alternâncias de contexto exigidas pela alocação de memória e também porque você pode usar suas próprias técnicas para impor contiguidade ou alinhamento específico para operações SIMD. Alguns mecanismos também usam uma pilha de extremidade dupla, na qual um tipo de recurso é carregado da parte superior e o outro, da parte inferior. Talvez o LSR (Load and Stay Resident, o tipo de coisa que será necessária ao longo de todo o jogo) da parte superior e os dados por nível da parte inferior.
  • Memória de quadro único ou memória de quadro com buffer duplo: Memória para operações que ocorrem dentro de um ou dois ciclos de quadro. Isso é útil porque, em vez de ter que alocar / desalocar cada quadro, você pode simplesmente explodir os dados do último quadro, redefinindo o ponteiro usado para acompanhar a memória até o início do bloco.
  • Pools de objetos: um bloco de memória para muitos objetos do mesmo tamanho, como partículas, inimigos e projéteis. Isso é útil porque você pode obter contiguidade facilmente, encontrando o primeiro segmento não utilizado em seu pool. Eles também facilitam a iteração, porque cada objeto está em um deslocamento conhecido do último.

A maior coisa que o autor menciona é a fragmentação da memória. Isso é menos problemático se você estiver desenvolvendo para, por exemplo, um PC em que tenha algum tipo de backup de paginação de memória com o qual possa confiar, mas em um contexto de memória fixa como um console, há o risco de "falta de memória" ao tentar alocar um objeto grande porque sua memória é fragmentada de forma que apenas pequenos blocos contíguos estejam disponíveis. Para esse fim, ele recomenda que um alocador baseado em pilha, como acima, também inclua um método de desfragmentar periodicamente seu conteúdo.

Para obter mais informações sobre o código real envolvido nisso, recomendo o artigo de Christian Gyrling, "Estamos com falta de memória?" , que abrange técnicas para alocadores personalizados, principalmente da perspectiva de analisar padrões de uso de memória, mas isso também é aplicável à criação de uma solução personalizada para gerenciamento de memória.


fonte
1

Pelo que vi (mas não foi feito), cada jogo tende a herdar os mecanismos de alocação de uma estrutura, de um mecanismo de jogo da versão anterior (2010 -> 2011) ou obtém um conjunto de novos escritos especificamente para seus estrutura (quando as estruturas de dados são reutilizáveis ​​e de tamanho fixo ou de vários tipos e tamanhos variáveis).

Também tivemos alocadores diferentes para arquivos / componentes de som do que para níveis e outros objetos de jogo no mesmo projeto. Em outros projetos, os alocadores são herdados de bibliotecas externas apenas para os componentes gerenciados por essa lib.

A otimização realmente depende das suas necessidades. Mas geralmente a alocação é feita antes de entrar na cena do jogo e, em seguida, a memória é reutilizada. Alguns jogos podem não ter nenhum alocador personalizado. Mas para jogos de ação nos quais o processador, a memória e os recursos de dados estão orçados, você não pode perder tempo de processamento em grandes alocações, não pode desperdiçar memória para fragmentação e outros problemas.

Em relação aos exemplos, você deve simplesmente começar dando uma olhada no mecanismo de jogo OGRE 3D , que possui algumas opções para configurar alocadores de memória.

Coiote
fonte
0

O erro geralmente cometido é escrever seus próprios alocadores, para que você possa ter mais controle sobre quanta memória é usada por cada sistema e ter mais visibilidade do que está acontecendo. Uma maneira muito melhor de conseguir isso é usar um perfilador de memória. Existem muitos perfis de memória por aí, o meu MemPro é um exemplo. Essa é uma maneira totalmente não invasiva de controlar todo o uso de memória, e você pode dividi-la automaticamente em subsistemas usando filtros curinga de pilha de chamada. Idealmente, é melhor manter sua alocação de memória e rastreamento de memória totalmente separados, pois eles têm requisitos totalmente diferentes.

Dividir arbitrariamente sua memória em pools geralmente pode ser prejudicial, pois cada pool terá uma sobrecarga. Você pode acabar usando muito mais memória do que precisa sem realmente perceber. Para reduzir o desperdício, é sempre melhor agrupar tudo, a folga é então compartilhada por todo o sistema.

Os únicos motivos para usar alocadores personalizados são o desempenho da CPU (principalmente para a coerência do cache) e para limitar a fragmentação. Um exemplo perfeito disso é um sistema de partículas. Você deseja que todas as partículas sejam contíguas na memória e não deseja incluir na memória principal muitas alocações de curta duração. Outro bom exemplo de particionamento é uma linguagem de script.

Se você deseja um exemplo de substituição de malloc de uso geral, pode dar uma olhada no meu alocador VMem . Ele foi usado em vários jogos AAA enviados. Possui técnicas que limitam a fragmentação e mantêm a área de memória baixa, algo crítico para jogos de console. Também é muito rápido sob alta contenção de threads. Meu site possui uma extensa documentação sobre essas técnicas.

Stewart Lynch
fonte