优化ObjectPool和MemoryPool性能

优化热路径性能 O(1) 避免线性查找
This commit is contained in:
陈思海 2026-04-23 17:50:53 +08:00
parent 46194ddee8
commit d1d86adf09
2 changed files with 265 additions and 80 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine;
@ -14,8 +15,10 @@ namespace AlicizaX.ObjectPool
public T Obj;
public int SpawnCount;
public float LastUseTime;
public int NameHash;
public int NextSameName;
public int PrevByName;
public int NextByName;
public int PrevAvailableByName;
public int NextAvailableByName;
public int PrevUnused;
public int NextUnused;
public byte Flags;
@ -37,7 +40,9 @@ namespace AlicizaX.ObjectPool
private int m_FreeTop;
private readonly Dictionary<object, int> m_TargetMap;
private IntOpenHashMap m_NameMap;
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;
@ -64,7 +69,9 @@ namespace AlicizaX.ObjectPool
m_Slots = new ObjectSlot[initCap];
m_FreeStack = new int[initCap];
m_TargetMap = new Dictionary<object, int>(initCap, AlicizaX.ReferenceComparer<object>.Instance);
m_NameMap = new IntOpenHashMap(initCap);
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;
@ -139,25 +146,32 @@ namespace AlicizaX.ObjectPool
slot.Obj = obj;
slot.SpawnCount = spawned ? 1 : 0;
slot.LastUseTime = Time.realtimeSinceStartup;
slot.NameHash = (obj.Name ?? string.Empty).GetHashCode();
slot.NextSameName = -1;
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;
if (m_NameMap.TryGetValue(slot.NameHash, out int existingHead))
slot.NextSameName = existingHead;
m_NameMap.AddOrUpdate(slot.NameHash, 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
AddToUnusedList(idx);
MarkSlotAvailable(idx);
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
ValidateState();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -166,8 +180,10 @@ namespace AlicizaX.ObjectPool
public T Spawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
int nameHash = name.GetHashCode();
if (!m_NameMap.TryGetValue(nameHash, out int head)) return null;
if (m_AllowMultiSpawn)
return SpawnAny(name);
if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return null;
float now = Time.realtimeSinceStartup;
int current = head;
@ -175,13 +191,14 @@ namespace AlicizaX.ObjectPool
{
ref var slot = ref m_Slots[current];
if (slot.IsAlive() && string.Equals(slot.Obj.Name, name)
&& (m_AllowMultiSpawn || slot.SpawnCount == 0))
&& slot.SpawnCount == 0)
{
SpawnSlot(current, now);
ValidateState();
return slot.Obj;
}
current = slot.NextSameName;
current = slot.NextAvailableByName;
}
return null;
@ -193,17 +210,18 @@ namespace AlicizaX.ObjectPool
public bool CanSpawn(string name)
{
if (name == null) throw new GameFrameworkException("Name is invalid.");
int nameHash = name.GetHashCode();
if (!m_NameMap.TryGetValue(nameHash, out int head)) return false;
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() && string.Equals(slot.Obj.Name, name)
&& (m_AllowMultiSpawn || slot.SpawnCount == 0))
if (slot.IsAlive() && slot.SpawnCount == 0 && string.Equals(slot.Obj.Name, name))
return true;
current = slot.NextSameName;
current = slot.NextAvailableByName;
}
return false;
@ -226,6 +244,7 @@ namespace AlicizaX.ObjectPool
}
UnspawnSlot(idx);
ValidateState();
}
public override void Release()
@ -258,6 +277,7 @@ namespace AlicizaX.ObjectPool
{
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
ShrinkStorageIfEmpty();
ValidateState();
}
}
@ -301,7 +321,9 @@ namespace AlicizaX.ObjectPool
}
m_TargetMap.Clear();
m_NameMap.Clear();
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_PendingReleaseCount = 0;
@ -309,6 +331,7 @@ namespace AlicizaX.ObjectPool
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
ValidateState();
}
public override ObjectInfo[] GetAllObjectInfos()
@ -351,7 +374,7 @@ namespace AlicizaX.ObjectPool
{
ref var slot = ref m_Slots[idx];
if (slot.SpawnCount == 0)
RemoveFromUnusedList(idx);
MarkSlotUnavailable(idx);
slot.SpawnCount++;
slot.LastUseTime = now;
@ -371,7 +394,7 @@ namespace AlicizaX.ObjectPool
if (slot.SpawnCount < 0)
throw new GameFrameworkException($"Object '{slot.Obj.Name}' spawn count < 0.");
if (slot.SpawnCount == 0)
AddToUnusedList(idx);
MarkSlotAvailable(idx);
if (Count > m_Capacity && slot.SpawnCount == 0)
MarkRelease(Count - m_Capacity);
}
@ -409,9 +432,9 @@ namespace AlicizaX.ObjectPool
T obj = slot.Obj;
if (slot.SpawnCount == 0)
RemoveFromUnusedList(idx);
MarkSlotUnavailable(idx);
RemoveFromNameChain(idx);
RemoveFromAllNameChain(idx);
m_TargetMap.Remove(obj.Target);
obj.Release(false);
@ -420,8 +443,10 @@ namespace AlicizaX.ObjectPool
slot.Obj = null;
slot.SetAlive(false);
slot.SpawnCount = 0;
slot.NameHash = 0;
slot.NextSameName = -1;
slot.PrevByName = -1;
slot.NextByName = -1;
slot.PrevAvailableByName = -1;
slot.NextAvailableByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
@ -432,34 +457,35 @@ namespace AlicizaX.ObjectPool
ShrinkStorageIfEmpty();
}
private void RemoveFromNameChain(int idx)
private void RemoveFromAllNameChain(int idx)
{
ref var slot = ref m_Slots[idx];
int nameHash = slot.NameHash;
if (!m_NameMap.TryGetValue(nameHash, out int head)) return;
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
return;
if (head == idx)
int prev = slot.PrevByName;
int next = slot.NextByName;
if (prev >= 0)
{
m_NameMap.Remove(nameHash);
if (slot.NextSameName >= 0)
m_NameMap.AddOrUpdate(nameHash, slot.NextSameName);
m_Slots[prev].NextByName = next;
}
else
{
int current = head;
while (current >= 0)
{
ref var chainSlot = ref m_Slots[current];
if (chainSlot.NextSameName == idx)
{
chainSlot.NextSameName = slot.NextSameName;
break;
}
current = chainSlot.NextSameName;
}
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);
}
slot.NextSameName = -1;
if (next >= 0)
m_Slots[next].PrevByName = prev;
slot.PrevByName = -1;
slot.NextByName = -1;
}
private int ReleaseUnused(int maxReleaseCount, bool requireExpired, float expireThreshold)
@ -530,7 +556,9 @@ namespace AlicizaX.ObjectPool
m_Slots = new ObjectSlot[InitSlotCapacity];
m_FreeStack = new int[InitSlotCapacity];
m_NameMap = new IntOpenHashMap(InitSlotCapacity);
m_AllNameHeadMap.Clear();
m_AvailableNameHeadMap.Clear();
m_AvailableNameTailMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
@ -538,45 +566,112 @@ namespace AlicizaX.ObjectPool
m_LastBudgetScanStart = -1;
}
private void AddToUnusedList(int idx)
[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;
if (m_UnusedTail >= 0 && m_Slots[m_UnusedTail].LastUseTime <= slot.LastUseTime)
{
slot.PrevUnused = m_UnusedTail;
slot.NextUnused = -1;
if (m_UnusedTail >= 0)
m_Slots[m_UnusedTail].NextUnused = idx;
slot.PrevUnused = m_UnusedTail;
slot.NextUnused = -1;
m_UnusedTail = idx;
}
else
{
int current = m_UnusedHead;
int prev = -1;
while (current >= 0 && m_Slots[current].LastUseTime <= slot.LastUseTime)
{
prev = current;
current = m_Slots[current].NextUnused;
}
m_UnusedHead = idx;
slot.PrevUnused = prev;
slot.NextUnused = current;
if (prev >= 0)
m_Slots[prev].NextUnused = idx;
else
m_UnusedHead = idx;
if (current >= 0)
m_Slots[current].PrevUnused = idx;
else
m_UnusedTail = idx;
if (m_UnusedTail < 0)
m_UnusedTail = idx;
}
m_UnusedTail = idx;
}
private void RemoveFromUnusedList(int idx)
@ -604,6 +699,75 @@ namespace AlicizaX.ObjectPool
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;
}
}
}
}

View File

@ -8,11 +8,13 @@ namespace AlicizaX.ObjectPool
[UnityEngine.Scripting.Preserve]
internal sealed partial class ObjectPoolService : ServiceBase, IObjectPoolService, IServiceTickable
{
private const float DefaultAutoReleaseInterval = float.MaxValue;
private const int DefaultCapacity = int.MaxValue;
private const float DefaultExpireTime = float.MaxValue;
private readonly Dictionary<TypeNamePair, ObjectPoolBase> m_ObjectPools;
private readonly List<ObjectPoolBase> m_ObjectPoolList;
private readonly Dictionary<ObjectPoolBase, int> m_ObjectPoolIndexMap;
private readonly List<ObjectPoolBase> m_CachedAllObjectPools;
private readonly Comparison<ObjectPoolBase> m_ObjectPoolComparer;
@ -20,6 +22,7 @@ namespace AlicizaX.ObjectPool
{
m_ObjectPools = new Dictionary<TypeNamePair, ObjectPoolBase>();
m_ObjectPoolList = new List<ObjectPoolBase>();
m_ObjectPoolIndexMap = new Dictionary<ObjectPoolBase, int>(AlicizaX.ReferenceComparer<ObjectPoolBase>.Instance);
m_CachedAllObjectPools = new List<ObjectPoolBase>();
m_ObjectPoolComparer = ObjectPoolComparer;
}
@ -41,6 +44,7 @@ namespace AlicizaX.ObjectPool
m_ObjectPoolList[i].Shutdown();
m_ObjectPools.Clear();
m_ObjectPoolList.Clear();
m_ObjectPoolIndexMap.Clear();
m_CachedAllObjectPools.Clear();
}
@ -117,12 +121,13 @@ namespace AlicizaX.ObjectPool
var pool = new ObjectPool<T>(
options.Name ?? string.Empty,
options.AllowMultiSpawn,
options.AutoReleaseInterval ?? DefaultExpireTime,
options.AutoReleaseInterval ?? DefaultAutoReleaseInterval,
options.Capacity ?? DefaultCapacity,
options.ExpireTime ?? DefaultExpireTime,
options.Priority);
m_ObjectPools.Add(key, pool);
m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count);
m_ObjectPoolList.Add(pool);
return pool;
}
@ -138,12 +143,13 @@ namespace AlicizaX.ObjectPool
var pool = (ObjectPoolBase)Activator.CreateInstance(poolType,
options.Name ?? string.Empty,
options.AllowMultiSpawn,
options.AutoReleaseInterval ?? DefaultExpireTime,
options.AutoReleaseInterval ?? DefaultAutoReleaseInterval,
options.Capacity ?? DefaultCapacity,
options.ExpireTime ?? DefaultExpireTime,
options.Priority);
m_ObjectPools.Add(key, pool);
m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count);
m_ObjectPoolList.Add(pool);
return pool;
}
@ -217,12 +223,27 @@ namespace AlicizaX.ObjectPool
if (m_ObjectPools.TryGetValue(key, out var pool))
{
pool.Shutdown();
m_ObjectPoolList.Remove(pool);
RemovePoolFromList(pool);
m_ObjectPoolIndexMap.Remove(pool);
return m_ObjectPools.Remove(key);
}
return false;
}
private void RemovePoolFromList(ObjectPoolBase pool)
{
if (!m_ObjectPoolIndexMap.TryGetValue(pool, out int index))
return;
int lastIndex = m_ObjectPoolList.Count - 1;
ObjectPoolBase lastPool = m_ObjectPoolList[lastIndex];
m_ObjectPoolList[index] = lastPool;
m_ObjectPoolList.RemoveAt(lastIndex);
if (!ReferenceEquals(lastPool, pool))
m_ObjectPoolIndexMap[lastPool] = index;
}
private static void ValidateObjectType(Type objectType)
{
if (objectType == null)