我有一个数据类型,其生命周期未定义。因此它是一个类。但是,它被创建得非常频繁,这导致它不断分配内存。它还具有相当大的权重,这使其存在问题。
怎样才能减少此数据类型所需的分配次数,优化其分配和释放,并减少 GC 压力?(创建结构是行不通的,因为它需要不断复制数据,并且不可能始终能够通过 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);
}
}
我尝试将其内部数据封装在一个单独的类中,并通过对象池获取该类的实例,这样主类的权重应该仅为 SIZE_OF_LINK + SOME_META_DATA_SIZE。然而,这似乎根本没有改变这种情况,仍然有大量分配。
您可能过于关注每个 GC 分配的精确大小,但最好尽量减少 GC 分配的数量和代码的整体复杂性。由于您使用的是 C# 8,实际上不需要序列化您的类型
DamageData
,并且愿意编写unsafe
代码,因此我将建议一种替代的、更简单的设计:您可以将您的DamageDataInternal
类替换为unsafe fixed int values[DamageTypeLength];
某个私有结构字段中的字段。这种设计DamageData
会比您的理想解决方案稍大,但它仍然相当小,同时无需分配和处置任何额外的内存(无论是否池化)。假设你的
DamageType
代码看起来是这样的。首先添加一些编译时常量来指示它的长度:然后重新实现你的
DamageData
和DamageDataInternal
,如下所示,创建一个包含固定大小缓冲区的DamageDataInternal
私有字段:struct
我怀疑(但不确定)Unity 是否可以序列化固定大小的缓冲区字段。您在评论中提到我有另一种类型作为的初始化程序
DamageData
。如果是这样,它需要看起来像以下 DTO:要构建
DamageData
具有某些特定损坏的,您可以使用如下初始化程序:笔记:
您现有的
DamageData
类将其大部分数据保存在DamageDataInternal
使用租用float []
数组的池化类型中。这种方法不会减少GC 分配的数量(您仍然每次都会分配DamageData
),它只会减少每个 GC 分配的大小,但代价是池化分配。您写道,创建结构是行不通的,因为它需要不断复制数据。只要该
damageDataInternal
字段是私有的,并且您永远不会通过某些属性或方法返回它,这在实践中就不会成为问题,因为您永远不会真正复制它。结构的大小
DamageDataInternal
将是DamageTypeLength
整数,加上缓存总值的额外整数。因此总共 11 个整数 = 44 字节(更可能是 48 字节加上填充)。由于
DamageDataInternal
结构嵌入在DamageData
类中,因此此类的大小将是结构的大小DamageDataInternal
加上您存储在那里的其他内容,例如缓存的哈希码。您现有的
DamageData
类使用终结器来释放池化DamageDataInternal
和租用的float[]
数组。带有终结器的类会对 GC 性能产生负面影响,如析构函数是否会影响性能?中所述,并且不是确定性的,正如Guru Stron在评论中指出的那样。通过使用dispose 模式并调用
GC.SuppressFinalize(this);
可以避免GC 性能下降Dispose()
——但是您指出 的生存期DamageData
是未定义的,因此这似乎不可行,因为您永远不知道何时调用Dispose()
。用从未复制的较小固定大小缓冲区替换池化和租用的值似乎是一种合理的选择,可以消除最终确定性能损失。如果损坏类型数量太多,我的设计就会变得不那么有吸引力。
在您的问题中,您没有说明如何
DamageData
使用。也许可以用它代替它DamageDataStruct
,并通过使用属性和索引器来访问它来避免过度复制ref return
。固定大小的缓冲区只能是结构的实例字段,但是这些结构可以是类的实例字段,也可以直接在堆栈上分配。
在C# 12中,你可以
unsafe
使用内联数组来消除代码,如下图所示: