diff --git a/OpenRA.Game/Primitives/PriorityQueue.cs b/OpenRA.Game/Primitives/PriorityQueue.cs index 25b72af22f..e69a3c2425 100644 --- a/OpenRA.Game/Primitives/PriorityQueue.cs +++ b/OpenRA.Game/Primitives/PriorityQueue.cs @@ -22,19 +22,39 @@ namespace OpenRA.Primitives T Pop(); } - public class PriorityQueue : IPriorityQueue + /// + /// Represents a collection of items that have a priority. + /// On pop, the item with the lowest priority value is removed. + /// + public sealed class PriorityQueue : IPriorityQueue where TComparer : struct, IComparer { - readonly List items; - readonly IComparer comparer; - int level, index; + /// + /// Compares two items to determine their priority. + /// PERF: Using a struct allows the calls to be devirtualized. + /// + readonly TComparer comparer; - public PriorityQueue() - : this(Comparer.Default) { } + /// + /// A binary min-heap storing the items. + /// An array divided into sub arrays called levels. At each level the size of a level array doubles. + /// Elements at deeper levels always have higher priority values than elements nearer to the root. + /// + T[] items; - public PriorityQueue(IComparer comparer) + /// + /// Index of deepest level. + /// + int level; + + /// + /// Number of elements in the deepest level. + /// + int index; + + public PriorityQueue(TComparer comparer) { - items = new List { new T[1] }; this.comparer = comparer; + items = new T[1]; } public void Add(T item) @@ -42,29 +62,37 @@ namespace OpenRA.Primitives var addLevel = level; var addIndex = index; - while (addLevel >= 1 && comparer.Compare(Above(addLevel, addIndex), item) > 0) + while (addLevel >= 1) { - items[addLevel][addIndex] = Above(addLevel, addIndex); - --addLevel; - addIndex >>= 1; + var above = items[AboveIndex(addLevel, addIndex)]; + if (comparer.Compare(above, item) > 0) + { + items[Index(addLevel, addIndex)] = above; + --addLevel; + addIndex >>= 1; + } + else + break; } - items[addLevel][addIndex] = item; + items[Index(addLevel, addIndex)] = item; if (++index >= 1 << level) { index = 0; - if (items.Count <= ++level) - items.Add(new T[1 << level]); + var count = 2 * (1 << ++level); + if (count - 1 >= items.Length) + Array.Resize(ref items, count); } } public bool Empty => level == 0; - T At(int level, int index) { return items[level][index]; } - T Above(int level, int index) { return items[level - 1][index >> 1]; } + static int Index(int level, int index) { return (1 << level) - 1 + index; } - T Last() + static int AboveIndex(int level, int index) { return (1 << (level - 1)) - 1 + (index >> 1); } + + int IndexLast() { var lastLevel = level; var lastIndex = index; @@ -72,20 +100,21 @@ namespace OpenRA.Primitives if (--lastIndex < 0) lastIndex = (1 << --lastLevel) - 1; - return At(lastLevel, lastIndex); + return Index(lastLevel, lastIndex); } public T Peek() { if (level <= 0 && index <= 0) throw new InvalidOperationException("PriorityQueue empty."); - return At(0, 0); + + return items[Index(0, 0)]; } public T Pop() { var ret = Peek(); - BubbleInto(0, 0, Last()); + BubbleInto(0, 0, items[IndexLast()]); if (--index < 0) index = (1 << --level) - 1; return ret; @@ -93,27 +122,38 @@ namespace OpenRA.Primitives void BubbleInto(int intoLevel, int intoIndex, T val) { - var downLevel = intoLevel + 1; - var downIndex = intoIndex << 1; - - if (downLevel > level || (downLevel == level && downIndex >= index)) + while (true) { - items[intoLevel][intoIndex] = val; - return; + var downLevel = intoLevel + 1; + var downIndex = intoIndex << 1; + + if (downLevel > level || (downLevel == level && downIndex >= index)) + { + items[Index(intoLevel, intoIndex)] = val; + return; + } + + var down = items[Index(downLevel, downIndex)]; + if (downLevel < level || (downLevel == level && downIndex < index - 1)) + { + var downRight = items[Index(downLevel, downIndex + 1)]; + if (comparer.Compare(down, downRight) >= 0) + { + down = downRight; + ++downIndex; + } + } + + if (comparer.Compare(val, down) <= 0) + { + items[Index(intoLevel, intoIndex)] = val; + return; + } + + items[Index(intoLevel, intoIndex)] = down; + intoLevel = downLevel; + intoIndex = downIndex; } - - if ((downLevel < level || (downLevel == level && downIndex < index - 1)) && - comparer.Compare(At(downLevel, downIndex), At(downLevel, downIndex + 1)) >= 0) - ++downIndex; - - if (comparer.Compare(val, At(downLevel, downIndex)) <= 0) - { - items[intoLevel][intoIndex] = val; - return; - } - - items[intoLevel][intoIndex] = At(downLevel, downIndex); - BubbleInto(downLevel, downIndex, val); } } } diff --git a/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs b/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs index 30e652b56e..5c797b0ede 100644 --- a/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/IPathGraph.cs @@ -77,12 +77,8 @@ namespace OpenRA.Mods.Common.Pathfinder /// public readonly struct GraphConnection { - public static readonly CostComparer ConnectionCostComparer = CostComparer.Instance; - - public sealed class CostComparer : IComparer + public readonly struct CostComparer : IComparer { - public static readonly CostComparer Instance = new CostComparer(); - CostComparer() { } public int Compare(GraphConnection x, GraphConnection y) { return x.Cost.CompareTo(y.Cost); diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index eea0d60fc8..6bd19dd724 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -170,7 +170,7 @@ namespace OpenRA.Mods.Common.Pathfinder this.heuristicWeightPercentage = heuristicWeightPercentage; TargetPredicate = targetPredicate; this.recorder = recorder; - openQueue = new PriorityQueue(GraphConnection.ConnectionCostComparer); + openQueue = new Primitives.PriorityQueue(default); } void AddInitialCell(CPos location, Func customCost) diff --git a/OpenRA.Test/OpenRA.Game/PriorityQueueTest.cs b/OpenRA.Test/OpenRA.Game/PriorityQueueTest.cs index 7ee0fa1af3..7cbfa78409 100644 --- a/OpenRA.Test/OpenRA.Game/PriorityQueueTest.cs +++ b/OpenRA.Test/OpenRA.Game/PriorityQueueTest.cs @@ -10,10 +10,10 @@ #endregion using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using OpenRA.Mods.Common; -using OpenRA.Primitives; using OpenRA.Support; namespace OpenRA.Test @@ -21,6 +21,11 @@ namespace OpenRA.Test [TestFixture] class PriorityQueueTest { + readonly struct Int32Comparer : IComparer + { + public int Compare(int x, int y) => x.CompareTo(y); + } + [TestCase(1, 123)] [TestCase(1, 1234)] [TestCase(1, 12345)] @@ -51,7 +56,7 @@ namespace OpenRA.Test var values = Enumerable.Range(0, count); var shuffledValues = values.Shuffle(mt).ToArray(); - var queue = new PriorityQueue(); + var queue = new Primitives.PriorityQueue(default); Assert.IsTrue(queue.Empty, "New queue should start out empty."); Assert.Throws(() => queue.Peek(), "Peeking at an empty queue should throw."); @@ -95,7 +100,7 @@ namespace OpenRA.Test var mt = new MersenneTwister(seed); var shuffledValues = Enumerable.Range(0, count).Shuffle(mt).ToArray(); - var queue = new PriorityQueue(); + var queue = new Primitives.PriorityQueue(default); Assert.IsTrue(queue.Empty, "New queue should start out empty."); Assert.Throws(() => queue.Peek(), "Peeking at an empty queue should throw.");