From d1d86adf09d5b3624469ef9115cb94e3209bbc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Thu, 23 Apr 2026 17:50:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ObjectPool=E5=92=8CMemoryPool?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化热路径性能 O(1) 避免线性查找 --- .../ObjectPoolService.ObjectPool.cs | 318 +++++++++++++----- Runtime/ObjectPool/ObjectPoolService.cs | 27 +- 2 files changed, 265 insertions(+), 80 deletions(-) diff --git a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs index 69e9882..a9557df 100644 --- a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs +++ b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs @@ -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 m_TargetMap; - private IntOpenHashMap m_NameMap; + private readonly Dictionary m_AllNameHeadMap; + private readonly Dictionary m_AvailableNameHeadMap; + private readonly Dictionary 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(initCap, AlicizaX.ReferenceComparer.Instance); - m_NameMap = new IntOpenHashMap(initCap); + m_AllNameHeadMap = new Dictionary(initCap, StringComparer.Ordinal); + m_AvailableNameHeadMap = new Dictionary(initCap, StringComparer.Ordinal); + m_AvailableNameTailMap = new Dictionary(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; + } } } } diff --git a/Runtime/ObjectPool/ObjectPoolService.cs b/Runtime/ObjectPool/ObjectPoolService.cs index c78ec2e..93f85b2 100644 --- a/Runtime/ObjectPool/ObjectPoolService.cs +++ b/Runtime/ObjectPool/ObjectPoolService.cs @@ -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 m_ObjectPools; private readonly List m_ObjectPoolList; + private readonly Dictionary m_ObjectPoolIndexMap; private readonly List m_CachedAllObjectPools; private readonly Comparison m_ObjectPoolComparer; @@ -20,6 +22,7 @@ namespace AlicizaX.ObjectPool { m_ObjectPools = new Dictionary(); m_ObjectPoolList = new List(); + m_ObjectPoolIndexMap = new Dictionary(AlicizaX.ReferenceComparer.Instance); m_CachedAllObjectPools = new List(); 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( 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)