Tenho um tipo de dado cujo tempo de vida é indefinido. Portanto, é uma classe. No entanto, ele é criado com muita frequência, o que faz com que ele aloque memória constantemente. Ele também tem bastante peso, o que o torna problemático.
O que pode ser feito para reduzir o número de alocações necessárias para esse tipo de dado, otimizar suas alocações e desalocações e reduzir a pressão do GC? (Criar uma estrutura não funcionaria, pois exigiria cópia constante de dados, com a impossibilidade de sempre ser capaz de liberar dados do array interno via try-finally).
[Serializable]
public class DamageData
{
[Serializable]
public class DamageDataInternal
{
public float[] values;
public float totalDamage;
public int cachedHashCode;
public bool isHashCodeDirty;
}
private DamageDataInternal _internalData;
public static readonly DamageType[] DamageTypes;
public int Hash => GetHashCode();
public bool IsInit => _internalData != null;
static DamageData()
{
DamageTypes = (DamageType[])Enum.GetValues(typeof(DamageType));
}
~DamageData()
{
DamageDataInternalPool.Shared.Release(_internalData);
_internalData = null;
}
public DamageData()
{
_internalData = DamageDataInternalPool.Shared.Get();
_internalData.totalDamage = 0f;
_internalData.isHashCodeDirty = true;
_internalData.cachedHashCode = 0;
}
}
using UnityEngine.Pool;
public class DamageDataInternalPool
{
private readonly ObjectPool<DamageData.DamageDataInternal> _pool;
public static DamageDataInternalPool Shared { get; } = new();
public DamageDataInternalPool()
{
_pool = new ObjectPool<DamageData.DamageDataInternal>(
createFunc: () => new DamageData.DamageDataInternal(),
actionOnGet: obj =>
{
obj.totalDamage = default;
obj.cachedHashCode = default;
obj.isHashCodeDirty = default;
obj.values = ArrayPool<float>.Shared.Rent(DamageData.DamageTypes.Length);
for (var i = 0; i < DamageData.DamageTypes.Length; i++)
{
obj.values[i] = 0f;
}
},
actionOnRelease: obj =>
{
ArrayPool<float>.Shared.Return(obj.values);
},
actionOnDestroy: null,
defaultCapacity: 10,
maxSize: 10000
);
}
public DamageData.DamageDataInternal Get()
{
return _pool.Get();
}
public void Release(DamageData.DamageDataInternal obj)
{
_pool.Release(obj);
}
}
Tentei encapsular seus dados internos em uma classe separada, instâncias das quais obtenho via object-pooling, para que a classe principal pesasse apenas SIZE_OF_LINK + SOME_META_DATA_SIZE. No entanto, isso não parece ter mudado a situação em nada e ainda há muita alocação.
Você pode estar focando muito no tamanho preciso de cada alocação de GC, mas pode ser uma ideia melhor minimizar o número de alocações de GC e a complexidade geral do seu código. Como você está trabalhando em C# 8, não precisa realmente serializar seu
DamageData
tipo e está aberto a escreverunsafe
código, vou sugerir um design alternativo mais simples: você pode substituir suaDamageDataInternal
classe por umunsafe fixed int values[DamageTypeLength];
campo em algum campo de struct privado. Com esse design,DamageData
será um pouco maior do que sua solução ideal, mas ainda será bem pequeno, eliminando a necessidade de alocar e descartar qualquer memória adicional, seja ela agrupada ou não.Digamos que seu
DamageType
se parece com algo assim. Primeiro adicione alguma constante de tempo de compilação indicando seu comprimento:Em seguida, reimplemente seu
DamageData
andDamageDataInternal
da seguinte maneira, criando um campoDamageDataInternal
privado contendo um buffer de tamanho fixo:struct
Duvido (mas não sei ao certo) que campos de buffer de tamanho fixo possam ser serializados pelo Unity. Você mencionou nos comentários que eu tenho outro tipo que é o inicializador para
DamageData
. Se sim, ele precisaria ser parecido com o seguinte DTO:Para construir um
DamageData
com algum dano específico, você pode usar um inicializador como este:Notas:
Sua
DamageData
classe existente mantém a maioria de seus dados em umDamageDataInternal
tipo agrupado que usa umfloat []
array alugado. Essa abordagem não diminui o número de alocações de GC (você ainda aloca umaDamageData
a cada vez), ela apenas diminui o tamanho de cada alocação de GC, ao custo de alocações de pool.Você escreveu, Fazer uma estrutura não funcionaria, pois exigiria cópia constante de dados . Contanto que o
damageDataInternal
campo seja privado e você nunca o retorne por meio de alguma propriedade ou método, isso não será um problema na prática, pois você nunca o copiará de fato.O tamanho da
DamageDataInternal
struct seráDamageTypeLength
inteiros, mais um inteiro extra para o valor total armazenado em cache. Então, 11 inteiros no total = 44 bytes (mais provavelmente 48 bytes com preenchimento).Como a
DamageDataInternal
estrutura está incorporada naDamageData
classe, o tamanho desta classe será o tamanhoDamageDataInternal
mais qualquer outra coisa que você armazenar lá, como um código hash em cache.Sua
DamageData
classe existente usa um finalizador para liberar o array agrupadoDamageDataInternal
e alugado . Classes com finalizadores podem impactar negativamente o desempenho do GC, conforme explicado em Os destrutores afetam o desempenho?, e não são determinísticos, conforme apontado nos comentários de Guru Stron .float[]
O impacto no desempenho do GC pode ser evitado usando o padrão discard e chamando
GC.SuppressFinalize(this);
fromDispose()
-- no entanto, você declarou que o tempo de vida deDamageData
é undefined , então isso não parece viável, pois você nunca saberá quando chamarDispose()
. Substituir os valores agrupados e alugados por um buffer de tamanho fixo pequeno que nunca é copiado parece uma alternativa razoável que elimina a penalidade de desempenho de finalização.Se você tivesse um número muito maior de tipos de dano, meu design seria menos atraente.
Na sua pergunta, você não mostra como
DamageData
é usado. Pode ser possível substituí-lo porDamageDataStruct
e evitar cópias em excesso usandoref return
propriedades e indexadores para acessá-lo.Buffers de tamanho fixo podem ser apenas campos de instância de structs, no entanto, esses structs podem ser campos de instância de classes, bem como alocados diretamente na pilha.
No C# 12 você pode eliminar o
unsafe
código usando um array embutido como mostrado neste violino :