com.alicizax.unity.framework/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs
陈思海 d1d86adf09 优化ObjectPool和MemoryPool性能
优化热路径性能 O(1) 避免线性查找
2026-04-23 17:50:53 +08:00

774 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace AlicizaX.ObjectPool
{
internal sealed partial class ObjectPoolService
{
private sealed class ObjectPool<T> : ObjectPoolBase, IObjectPool<T> where T : ObjectBase
{
private struct ObjectSlot
{
public T Obj;
public int SpawnCount;
public float LastUseTime;
public int PrevByName;
public int NextByName;
public int PrevAvailableByName;
public int NextAvailableByName;
public int PrevUnused;
public int NextUnused;
public byte Flags;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsAlive() => (Flags & 1) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetAlive(bool alive)
{
if (alive) Flags |= 1;
else Flags = 0;
}
}
private ObjectSlot[] m_Slots;
private int m_SlotCount;
private int[] m_FreeStack;
private int m_FreeTop;
private readonly Dictionary<object, int> m_TargetMap;
private readonly Dictionary<string, int> m_AllNameHeadMap;
private readonly Dictionary<string, int> m_AvailableNameHeadMap;
private readonly Dictionary<string, int> m_AvailableNameTailMap;
private readonly bool m_AllowMultiSpawn;
private float m_AutoReleaseInterval;
private int m_Capacity;
private float m_ExpireTime;
private int m_Priority;
private float m_AutoReleaseTime;
private int m_PendingReleaseCount;
private int m_ReleasePerFrameBudget;
private int m_UnusedHead;
private int m_UnusedTail;
private int m_LastBudgetScanStart;
private bool m_IsShuttingDown;
private const int DefaultReleasePerFrame = 8;
private const int InitSlotCapacity = 16;
public ObjectPool(string name, bool allowMultiSpawn,
float autoReleaseInterval, int capacity, float expireTime, int priority)
: base(name)
{
int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity);
m_Slots = new ObjectSlot[initCap];
m_FreeStack = new int[initCap];
m_TargetMap = new Dictionary<object, int>(initCap, AlicizaX.ReferenceComparer<object>.Instance);
m_AllNameHeadMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_AvailableNameHeadMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_AvailableNameTailMap = new Dictionary<string, int>(initCap, StringComparer.Ordinal);
m_AllowMultiSpawn = allowMultiSpawn;
m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity;
m_ExpireTime = expireTime;
m_Priority = priority;
m_AutoReleaseTime = 0f;
m_PendingReleaseCount = 0;
m_ReleasePerFrameBudget = DefaultReleasePerFrame;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
}
public override Type ObjectType => typeof(T);
public override int Count => m_TargetMap.Count;
public override bool AllowMultiSpawn => m_AllowMultiSpawn;
public override float AutoReleaseInterval
{
get => m_AutoReleaseInterval;
set
{
if (value < 0f) throw new GameFrameworkException("AutoReleaseInterval is invalid.");
m_AutoReleaseInterval = value;
}
}
public override int Capacity
{
get => m_Capacity;
set
{
if (value < 0) throw new GameFrameworkException("Capacity is invalid.");
m_Capacity = value;
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
}
}
public override float ExpireTime
{
get => m_ExpireTime;
set
{
if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid.");
m_ExpireTime = value;
}
}
public override int Priority
{
get => m_Priority;
set => m_Priority = value;
}
public override int ReleasePerFrameBudget
{
get => m_ReleasePerFrameBudget;
set => m_ReleasePerFrameBudget = Math.Max(1, value);
}
public void Register(T obj, bool spawned)
{
if (obj == null) throw new GameFrameworkException("Object is invalid.");
if (obj.Target == null) throw new GameFrameworkException("Object target is invalid.");
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive())
throw new GameFrameworkException(
$"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'.");
int idx = AllocSlot();
ref var slot = ref m_Slots[idx];
slot.Obj = obj;
slot.SpawnCount = spawned ? 1 : 0;
slot.LastUseTime = Time.realtimeSinceStartup;
slot.PrevByName = -1;
slot.NextByName = -1;
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
slot.SetAlive(true);
m_TargetMap[obj.Target] = idx;
string objectName = obj.Name ?? string.Empty;
if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead))
{
m_Slots[existingHead].PrevByName = idx;
slot.NextByName = existingHead;
}
m_AllNameHeadMap[objectName] = idx;
obj.LastUseTime = slot.LastUseTime;
if (spawned)
obj.OnSpawn();
else
MarkSlotAvailable(idx);
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
ValidateState();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Spawn() => Spawn(string.Empty);
public T Spawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
if (m_AllowMultiSpawn)
return SpawnAny(name);
if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return null;
float now = Time.realtimeSinceStartup;
int current = head;
while (current >= 0)
{
ref var slot = ref m_Slots[current];
if (slot.IsAlive() && string.Equals(slot.Obj.Name, name)
&& slot.SpawnCount == 0)
{
SpawnSlot(current, now);
ValidateState();
return slot.Obj;
}
current = slot.NextAvailableByName;
}
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanSpawn() => CanSpawn(string.Empty);
public bool CanSpawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
if (m_AllowMultiSpawn)
return m_AllNameHeadMap.ContainsKey(name);
if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return false;
int current = head;
while (current >= 0)
{
ref var slot = ref m_Slots[current];
if (slot.IsAlive() && slot.SpawnCount == 0 && string.Equals(slot.Obj.Name, name))
return true;
current = slot.NextAvailableByName;
}
return false;
}
public void Unspawn(T obj)
{
if (obj == null) throw new GameFrameworkException("Object is invalid.");
Unspawn(obj.Target);
}
public void Unspawn(object target)
{
if (target == null) throw new GameFrameworkException("Target is invalid.");
if (!m_TargetMap.TryGetValue(target, out int idx))
{
if (m_IsShuttingDown) return;
throw new GameFrameworkException(
$"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'");
}
UnspawnSlot(idx);
ValidateState();
}
public override void Release()
{
MarkRelease(Count - m_Capacity);
}
public override void Release(int toReleaseCount)
{
MarkRelease(toReleaseCount);
}
public override void ReleaseAllUnused()
{
int released = 0;
int current = m_UnusedHead;
while (current >= 0)
{
int next = m_Slots[current].NextUnused;
ref var slot = ref m_Slots[current];
if (CanReleaseSlot(ref slot))
{
ReleaseSlot(current);
released++;
}
current = next;
}
if (released > 0)
{
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
ShrinkStorageIfEmpty();
ValidateState();
}
}
internal override void Update(float elapseSeconds, float realElapseSeconds)
{
m_AutoReleaseTime += realElapseSeconds;
if (m_AutoReleaseTime >= m_AutoReleaseInterval)
{
m_AutoReleaseTime = 0f;
MarkRelease(Count - m_Capacity);
}
bool checkExpire = m_ExpireTime < float.MaxValue;
if (m_PendingReleaseCount <= 0 && !checkExpire) return;
float now = Time.realtimeSinceStartup;
float expireThreshold = checkExpire ? now - m_ExpireTime : float.MinValue;
if (m_PendingReleaseCount > 0)
{
int releaseBudget = Math.Min(m_ReleasePerFrameBudget, m_PendingReleaseCount);
int releasedByBudget = ReleaseUnused(releaseBudget, false, float.MinValue);
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - releasedByBudget);
}
else if (checkExpire)
{
ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold);
}
}
internal override void Shutdown()
{
m_IsShuttingDown = true;
for (int i = 0; i < m_SlotCount; i++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
slot.Obj.Release(true);
MemoryPool.Release(slot.Obj);
slot.Obj = null;
slot.SetAlive(false);
}
m_TargetMap.Clear();
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_PendingReleaseCount = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
ValidateState();
}
public override ObjectInfo[] GetAllObjectInfos()
{
var list = new ObjectInfo[m_TargetMap.Count];
int write = 0;
for (int i = 0; i < m_SlotCount && write < list.Length; i++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
list[write++] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount);
}
return list;
}
public override void GetAllObjectInfos(List<ObjectInfo> results)
{
if (results == null) throw new GameFrameworkException("Results is invalid.");
results.Clear();
for (int i = 0; i < m_SlotCount; i++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
results.Add(new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount));
}
}
internal override void OnLowMemory()
{
ReleaseAllUnused();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SpawnSlot(int idx, float now)
{
ref var slot = ref m_Slots[idx];
if (slot.SpawnCount == 0)
MarkSlotUnavailable(idx);
slot.SpawnCount++;
slot.LastUseTime = now;
slot.Obj.LastUseTime = now;
slot.Obj.OnSpawn();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UnspawnSlot(int idx)
{
ref var slot = ref m_Slots[idx];
float now = Time.realtimeSinceStartup;
slot.LastUseTime = now;
slot.Obj.LastUseTime = now;
slot.Obj.OnUnspawn();
slot.SpawnCount--;
if (slot.SpawnCount < 0)
throw new GameFrameworkException($"Object '{slot.Obj.Name}' spawn count < 0.");
if (slot.SpawnCount == 0)
MarkSlotAvailable(idx);
if (Count > m_Capacity && slot.SpawnCount == 0)
MarkRelease(Count - m_Capacity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void MarkRelease(int count)
{
if (count > 0)
m_PendingReleaseCount = Math.Max(m_PendingReleaseCount, count);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int AllocSlot()
{
if (m_FreeTop > 0)
return m_FreeStack[--m_FreeTop];
if (m_SlotCount >= m_Slots.Length)
GrowSlots();
return m_SlotCount++;
}
private void GrowSlots()
{
int newCap = Math.Max(m_Slots.Length * 2, InitSlotCapacity);
Array.Resize(ref m_Slots, newCap);
Array.Resize(ref m_FreeStack, newCap);
}
private void ReleaseSlot(int idx)
{
ref var slot = ref m_Slots[idx];
if (!slot.IsAlive()) return;
T obj = slot.Obj;
if (slot.SpawnCount == 0)
MarkSlotUnavailable(idx);
RemoveFromAllNameChain(idx);
m_TargetMap.Remove(obj.Target);
obj.Release(false);
MemoryPool.Release(obj);
slot.Obj = null;
slot.SetAlive(false);
slot.SpawnCount = 0;
slot.PrevByName = -1;
slot.NextByName = -1;
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
if (m_FreeTop >= m_FreeStack.Length)
Array.Resize(ref m_FreeStack, m_FreeStack.Length * 2);
m_FreeStack[m_FreeTop++] = idx;
ShrinkStorageIfEmpty();
}
private void RemoveFromAllNameChain(int idx)
{
ref var slot = ref m_Slots[idx];
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
return;
int prev = slot.PrevByName;
int next = slot.NextByName;
if (prev >= 0)
{
m_Slots[prev].NextByName = next;
}
else
{
if (head != idx)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain is inconsistent.");
if (next >= 0)
m_AllNameHeadMap[objectName] = next;
else
m_AllNameHeadMap.Remove(objectName);
}
if (next >= 0)
m_Slots[next].PrevByName = prev;
slot.PrevByName = -1;
slot.NextByName = -1;
}
private int ReleaseUnused(int maxReleaseCount, bool requireExpired, float expireThreshold)
{
int released = 0;
int current = requireExpired ? m_UnusedHead : GetBudgetScanStart();
while (current >= 0 && released < maxReleaseCount)
{
ref var slot = ref m_Slots[current];
int next = slot.NextUnused;
if (requireExpired && slot.LastUseTime > expireThreshold)
{
break;
}
if (CanReleaseSlot(ref slot))
{
ReleaseSlot(current);
released++;
}
current = next;
}
if (!requireExpired)
{
m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead;
}
return released;
}
private void ReleaseExpired(int maxReleaseCount, float expireThreshold)
{
ReleaseUnused(maxReleaseCount, true, expireThreshold);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetBudgetScanStart()
{
if (m_LastBudgetScanStart >= 0)
{
ref var slot = ref m_Slots[m_LastBudgetScanStart];
if (slot.IsAlive() && slot.SpawnCount == 0)
{
return m_LastBudgetScanStart;
}
}
return m_UnusedHead;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanReleaseSlot(ref ObjectSlot slot)
{
return slot.IsAlive()
&& slot.SpawnCount == 0
&& !slot.Obj.Locked
&& slot.Obj.CustomCanReleaseFlag;
}
private void ShrinkStorageIfEmpty()
{
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
return;
m_Slots = new ObjectSlot[InitSlotCapacity];
m_FreeStack = new int[InitSlotCapacity];
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
private void ValidateState()
{
int aliveCount = 0;
int unusedCount = 0;
for (int i = 0; i < m_SlotCount; i++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive())
continue;
aliveCount++;
object target = slot.Obj.Target;
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i)
throw new GameFrameworkException($"Object pool '{FullName}' target index map is inconsistent.");
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
throw new GameFrameworkException($"Object pool '{FullName}' all-name head is missing.");
if (slot.PrevByName < 0 && head != i)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain head is inconsistent.");
if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i)
throw new GameFrameworkException($"Object pool '{FullName}' all-name chain link is inconsistent.");
bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
bool inAvailableList = false;
if (slot.SpawnCount == 0)
{
unusedCount++;
if (!inUnusedList)
throw new GameFrameworkException($"Object pool '{FullName}' unused list is inconsistent.");
if (!m_AvailableNameHeadMap.TryGetValue(objectName, out int availableHead))
throw new GameFrameworkException($"Object pool '{FullName}' available-name head is missing.");
inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0;
if (!inAvailableList)
throw new GameFrameworkException($"Object pool '{FullName}' available-name chain is inconsistent.");
if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i)
throw new GameFrameworkException($"Object pool '{FullName}' available-name link is inconsistent.");
}
else
{
if (inUnusedList)
throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in unused list.");
if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in available chain.");
}
}
if (aliveCount != m_TargetMap.Count)
throw new GameFrameworkException($"Object pool '{FullName}' alive count is inconsistent.");
int walkUnusedCount = 0;
int current = m_UnusedHead;
int prevUnused = -1;
while (current >= 0)
{
ref var slot = ref m_Slots[current];
if (!slot.IsAlive() || slot.SpawnCount != 0)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain contains invalid slot.");
if (slot.PrevUnused != prevUnused)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain linkage is inconsistent.");
walkUnusedCount++;
prevUnused = current;
current = slot.NextUnused;
}
if (walkUnusedCount != unusedCount)
throw new GameFrameworkException($"Object pool '{FullName}' unused chain count is inconsistent.");
}
private void MarkSlotAvailable(int idx)
{
AddToUnusedListTail(idx);
AddToAvailableNameChain(idx);
}
private void MarkSlotUnavailable(int idx)
{
RemoveFromUnusedList(idx);
RemoveFromAvailableNameChain(idx);
}
private void AddToUnusedListTail(int idx)
{
ref var slot = ref m_Slots[idx];
if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0)
return;
slot.PrevUnused = m_UnusedTail;
slot.NextUnused = -1;
if (m_UnusedTail >= 0)
m_Slots[m_UnusedTail].NextUnused = idx;
else
m_UnusedHead = idx;
m_UnusedTail = idx;
}
private void RemoveFromUnusedList(int idx)
{
ref var slot = ref m_Slots[idx];
if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0)
return;
int prev = slot.PrevUnused;
int next = slot.NextUnused;
if (prev >= 0)
m_Slots[prev].NextUnused = next;
else
m_UnusedHead = next;
if (next >= 0)
m_Slots[next].PrevUnused = prev;
else
m_UnusedTail = prev;
slot.PrevUnused = -1;
slot.NextUnused = -1;
if (m_LastBudgetScanStart == idx)
m_LastBudgetScanStart = next >= 0 ? next : m_UnusedHead;
}
private void AddToAvailableNameChain(int idx)
{
ref var slot = ref m_Slots[idx];
if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
return;
string objectName = slot.Obj.Name ?? string.Empty;
if (m_AvailableNameTailMap.TryGetValue(objectName, out int tail))
{
m_Slots[tail].NextAvailableByName = idx;
slot.PrevAvailableByName = tail;
slot.NextAvailableByName = -1;
m_AvailableNameTailMap[objectName] = idx;
}
else
{
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
m_AvailableNameHeadMap[objectName] = idx;
m_AvailableNameTailMap[objectName] = idx;
}
}
private void RemoveFromAvailableNameChain(int idx)
{
ref var slot = ref m_Slots[idx];
string objectName = slot.Obj.Name ?? string.Empty;
if (slot.PrevAvailableByName < 0
&& slot.NextAvailableByName < 0
&& (!m_AvailableNameHeadMap.TryGetValue(objectName, out int head) || head != idx))
{
return;
}
int prev = slot.PrevAvailableByName;
int next = slot.NextAvailableByName;
if (prev >= 0)
m_Slots[prev].NextAvailableByName = next;
else if (next >= 0)
m_AvailableNameHeadMap[objectName] = next;
else
m_AvailableNameHeadMap.Remove(objectName);
if (next >= 0)
m_Slots[next].PrevAvailableByName = prev;
else if (prev >= 0)
m_AvailableNameTailMap[objectName] = prev;
else
m_AvailableNameTailMap.Remove(objectName);
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
}
private T SpawnAny(string name)
{
if (!m_AllNameHeadMap.TryGetValue(name, out int head))
return null;
float now = Time.realtimeSinceStartup;
ref var slot = ref m_Slots[head];
if (!slot.IsAlive() || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
return null;
SpawnSlot(head, now);
return slot.Obj;
}
}
}
}