Como devo explicar o GC ao criar jogos com o Unity?

15

* Até onde eu sei, o Unity3D para iOS é baseado no tempo de execução do Mono e o Mono possui apenas GC geracional de marcação e varredura.

Este sistema de GC não pode evitar o tempo de GC, o que interrompe o sistema de jogo. O pool de instâncias pode reduzir isso, mas não completamente, porque não podemos controlar a instanciação na biblioteca de classes base do CLR. Essas instâncias pequenas e frequentes ocultas aumentarão o tempo não-determinístico do GC. Forçar o GC completo periodicamente prejudicará muito o desempenho (o Mono pode forçar o GC completo, na verdade?)

Então, como posso evitar esse tempo de GC ao usar o Unity3D sem degradar o desempenho?

Eonil
fonte
3
O mais recente Unity Pro Profiler inclui a quantidade de lixo gerada em determinadas chamadas de função. Você pode usar esta ferramenta para ajudar a ver outros lugares que podem estar gerando lixo que, caso contrário, não seriam imediatamente óbvios.
Tetrad

Respostas:

18

Felizmente, como você apontou, as compilações COMPACT Mono usam um GC geracional (em forte contraste com as da Microsoft, como o WinMo / WinPhone / XBox, que apenas mantém uma lista simples).

Se o seu jogo é simples, o GC deve lidar com isso muito bem, mas aqui estão algumas dicas que você pode querer examinar.

Otimização prematura

Primeiro, verifique se isso é realmente um problema para você antes de tentar corrigi-lo.

Agrupando tipos de referência caros

Você deve agrupar os tipos de referência que você cria com frequência ou que possuem estruturas profundas. Um exemplo de cada um seria:

  • Criado com freqüência: um Bulletobjeto em um jogo de balas .
  • Estrutura profunda: Árvore de decisão para uma implementação de IA.

Você deve usar a Stackcomo seu pool (diferente da maioria das implementações que usam a Queue). A razão para isso é porque, com a, Stackse você retorna um objeto para a piscina e outra coisa o agarra imediatamente; ele terá uma chance muito maior de estar em uma página ativa - ou mesmo no cache da CPU, se você tiver sorte. É um pouco mais rápido. Além disso, limite sempre o tamanho das suas piscinas (apenas desconsidere 'check-ins' se o seu limite tiver sido excedido).

Evite criar novas listas para eliminá-las

Não crie um novo Listquando você realmente quis fazer Clear()isso. Você pode reutilizar a matriz de back-end e salvar uma carga de alocações e cópias de matriz. Da mesma forma que isso, tente criar listas com uma capacidade inicial significativa (lembre-se, isso não é um limite - apenas uma capacidade inicial) - não precisa ser preciso, apenas uma estimativa. Isso deve se aplicar basicamente a qualquer tipo de coleção - exceto a LinkedList.

Use matrizes de estruturas (ou listas) sempre que possível

Você obtém pouco benefício com o uso de estruturas (ou tipos de valor em geral) se as distribuir entre objetos. Por exemplo, na maioria dos 'bons' sistemas de partículas, as partículas individuais são armazenadas em uma matriz massiva: a matriz e o índice são passados ​​ao invés da própria partícula. A razão pela qual isso funciona tão bem é porque, quando o GC precisa coletar a matriz, pode pular completamente o conteúdo (é uma matriz primitiva - nada a fazer aqui). Então, em vez de olhar para 10.000 objetos, o GC simplesmente precisa olhar para 1 matriz: enorme ganho! Novamente, isso funcionará apenas com tipos de valor .

Depois do RoyT. forneceu algum feedback viável e construtivo, sinto que preciso expandir mais isso. Você só deve usar essa técnica quando estiver lidando com grandes quantidades de entidades (milhares a dezenas de milhares). Além disso, deve ser uma estrutura que não deve ter nenhum campo de tipo de referência e deve estar em uma matriz de tipo explícito. Ao contrário do feedback dele, estamos colocando-o em uma matriz que provavelmente é um campo de uma classe - o que significa que ele vai acabar com o heap (não estamos tentando evitar uma alocação de heap - apenas evitando o trabalho do GC). Nós realmente nos preocupamos com o fato de que é um pedaço de memória contíguo com muitos valores que o GC pode simplesmente olhar em uma O(1)operação em vez de uma O(n)operação.

Você também deve alocar essas matrizes o mais próximo possível da inicialização do aplicativo para reduzir as chances de ocorrência de fragmentação ou de trabalho excessivo, pois o GC tenta mover esses pedaços, e considere usar uma lista vinculada híbrida em vez do Listtipo interno )

GC.Collect ()

Este é definitivamente o melhor maneira de dar um tiro no pé (veja: "Considerações sobre desempenho") com um GC de gerações. Você deve chamá-lo apenas quando tiver criado uma quantidade EXTREMA de lixo - e a única instância em que isso pode ser um problema é logo após o carregamento do conteúdo de um nível - e mesmo assim você provavelmente deve coletar apenas a primeira geração ( GC.Collect(0);) esperançosamente impedir a promoção de objetos para a terceira geração.

IDisposable e Anulação de Campo

Vale a pena anular campos quando você não precisa mais de um objeto (mais ainda em objetos restritos). O motivo está nos detalhes de como o GC funciona: ele remove apenas objetos que não estão enraizados (ou seja, referenciados), mesmo que esse objeto tenha sido desaprotegido por causa de outros objetos sendo removidos na coleção atual ( nota: isso depende do GC sabor em uso - alguns realmente limpam as correntes). Além disso, se um objeto sobreviver a uma coleção, ele será imediatamente promovido para a próxima geração - isso significa que qualquer objeto deixado nos campos será promovido durante uma coleção. Cada geração sucessiva é exponencialmente mais cara de coletar (e ocorre com pouca frequência).

Veja o seguinte exemplo:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// G1 Collection
MyNestObject (G2) -> MyFurtherNestedObject (G2)
// G2 Collection
MyFurtherNestedObject (G3)

E se MyFurtherNestedObject um objeto com vários megabytes, é possível garantir que o GC não o veja por um longo tempo - porque você o promoveu inadvertidamente para o G3. Compare isso com este exemplo:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// Dispose
MyObject (G1)
MyNestedObject (G1)
MyFurtherNestedObject (G1)
// G1 Collection

O padrão Disposer ajuda a configurar uma maneira previsível de solicitar aos objetos que limpem seus campos particulares. Por exemplo:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

    // A finalizer is only needed IF YOU CONTROL UNMANAGED RESOURCES
    // ~MyClass() { }

    public void Dispose()
    {
       _nested = null;
    }
}
Jonathan Dickinson
fonte
11
WTF você está falando? O .NET Framework no Windows é absolutamente geracional. Sua classe ainda tem métodos GetGeneration .
DeadMG 12/03
2
O .NET no Windows usa um Gerational Garbage Collector, no entanto, o .NET Compact Framework, usado no WP7 e no Xbox360, tem um GC mais limitado, embora provavelmente seja mais do que uma lista simples, não é um geracional completo.
Roy T.
Jonathan Dickinson: Gostaria de acrescentar que nem sempre as estruturas são a resposta. No .Net e, provavelmente, também no Mono, uma estrutura não vive necessariamente na pilha (o que é bom), mas também pode viver no heap (que é onde você precisa de um Garbage Collector). As regras para isso são bastante complicadas, mas geralmente ocorrem quando uma estrutura contém tipos sem valor.
Roy T.
11
@RoyT. veja o comentário acima. Eu indiquei que as estruturas são boas para uma grande quantidade de dados em um array - como um sistema de partículas. Se você está preocupado com as estruturas do GC em uma matriz, é o caminho a seguir; para dados como partículas.
12133 Jonathan Dickinson
10
O @DeadMG também é desnecessário para o WTF - considerando que você realmente não leu a resposta corretamente. Acalme seu temperamento e releia a resposta antes de escrever comentários odiosos em uma comunidade geralmente bem-comportada como esta.
12136 Jonathan Dickinson
7

Pouca instanciação dinâmica acontece automaticamente dentro da biblioteca base, a menos que você chame algo que exija. A grande maioria da alocação e desalocação de memória vem do seu próprio código e você pode controlar isso.

Pelo que entendi, você não pode evitar isso completamente - tudo o que você pode fazer é garantir que você recicle e agrupe objetos sempre que possível. Use estruturas em vez de classes, evite o sistema UnityGUI e geralmente evite criar novos objetos na memória dinâmica durante períodos de tempo. quando o desempenho é importante.

Você também pode forçar a coleta de lixo em determinados horários, o que pode ou não ajudar: consulte algumas das sugestões aqui: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
fonte
2
Você realmente deve explicar as implicações de forçar o GC. Ela causa estragos nas heurísticas e no layout do GC e pode levar a problemas de memória maiores se usados ​​incorretamente: a comunidade XNA / MVPs / staff geralmente considera o conteúdo pós-carregamento o único tempo razoável para forçar uma coleta. Você realmente deve evitar mencioná-lo inteiramente se dedicar apenas uma frase ao assunto. GC.Collect()= memória livre agora, mas problemas muito prováveis ​​mais tarde.
12133 Jonathan Dickinson
Não, você deve explicar as implicações de forçar a GC porque você sabe mais sobre isso do que eu. :) No entanto, lembre-se de que o GC Mono não é necessariamente o mesmo que o Microsoft GC e os riscos podem não ser os mesmos.
Kylotan
Ainda assim, +1. Fui adiante e elaborei algumas experiências pessoais com o GC - que você pode achar interessantes.
12136 Jonathan Dickinson