diff --git a/optimization_ideas.txt b/optimization_ideas.txt index 6feb84b0d..773adad1d 100644 --- a/optimization_ideas.txt +++ b/optimization_ideas.txt @@ -31,12 +31,12 @@ Thoughts concerning optimization: (but interestingly, not O(log log N)). However, I think this would be significantly slower than a B-tree in practice. - If I don't implement the B-tree optimization, there are a couple of small - optimizations I could try. First, I could move the field - EulerTourNode.augmentationFunc to EulerTourVertex, in order to save a little - bit of space. Second, I could create EulerTourNode and EulerTourVertex - subclasses specifically for the top level, and offload the fields related to - user augmentation to those subclasses. These changes seem inelegant, but it - might be worth checking the effect they have on performance. + optimizations I could try. First, I could move the field EulerTourNode.graph + to EulerTourVertex, in order to save a little bit of space. Second, I could + create EulerTourNode and EulerTourVertex subclasses specifically for the top + level, and offload the fields related to user augmentation to those + subclasses. These changes seem inelegant, but it might be worth checking the + effect they have on performance. - I looked at the heuristics recommended in http://people.csail.mit.edu/karger/Papers/impconn.pdf (Iyer, et al. (2001): An Experimental Study of Poly-Logarithmic Fully-Dynamic Connectivity Algorithms). diff --git a/src/main/java/com/github/btrekkie/connectivity/AugmentationPool.java b/src/main/java/com/github/btrekkie/connectivity/AugmentationPool.java new file mode 100644 index 000000000..293187421 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/AugmentationPool.java @@ -0,0 +1,53 @@ +package com.github.btrekkie.connectivity; + +/** + * An Augmentation implementation that wraps a MutatingAugmentation. It recycles (or "pools") previously constructed + * combined augmentation objects in order to improve performance. This should be passed to the ConnGraph constructor as + * an AugmentationReleaseListener so that it can recycle unused objects. + */ +class AugmentationPool implements Augmentation, AugmentationReleaseListener { + /** The maximum number of unused objects to store in a pool. */ + private static final int CAPACITY = 20; + + /** The MutatingAugmentation we are wrapping. */ + private final MutatingAugmentation mutatingAugmentation; + + /** + * A pool of unused objects we may reuse. The array has length CAPACITY, but only the first "size" elements contain + * reusable objects. The values in the array after the first "size" are unspecified. + */ + private Object[] pool = new Object[CAPACITY]; + + /** The number of reusable objects in the pool. */ + private int size; + + public AugmentationPool(MutatingAugmentation mutatingAugmentation) { + this.mutatingAugmentation = mutatingAugmentation; + } + + @Override + public Object combine(Object value1, Object value2) { + Object result; + if (size == 0) { + result = mutatingAugmentation.newAugmentation(); + } else { + size--; + result = pool[size]; + } + mutatingAugmentation.combine(value1, value2, result); + return result; + } + + @Override + public void combinedAugmentationReleased(Object obj) { + if (size < CAPACITY) { + pool[size] = obj; + size++; + } + } + + @Override + public void vertexAugmentationReleased(Object obj) { + + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/AugmentationReleaseListener.java b/src/main/java/com/github/btrekkie/connectivity/AugmentationReleaseListener.java new file mode 100644 index 000000000..662d9bebb --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/AugmentationReleaseListener.java @@ -0,0 +1,32 @@ +package com.github.btrekkie.connectivity; + +/** + * Responds to when ownership of an augmentation object is released. If the number of times that + * combinedAugmentationReleased and vertexAugmentationReleased were called on an object is equal to the number of times + * that Augmentation.combine returned the object plus the number of times the object was passed to + * setVertexAugmentation, then ConnGraph no longer has a reference to the object. Such an object may be recycled. + * + * Note that a graph may have multiple ownership claims to a given augmentation object, meaning the graph needs to + * release the object multiple times before it can be recycled. This could happen if Augmentation.combine returned the + * same object multiple times or the object was passed to setVertexAugmentation multiple times. + * + * See ConnGraph(Augmentation, AugmentationReleaseListener). + */ +public interface AugmentationReleaseListener { + /** + * Responds to one ownership claim to the specified combined augmentation object (or previous return value of + * Augmentation.combine) being released. "obj" is guaranteed not to be null. + * + * This may be called from any ConnGraph method that mutates the graph, as well as from optimize(). + */ + public void combinedAugmentationReleased(Object obj); + + /** + * Responds to one ownership claim to the specified vertex augmentation object (or previous argument to + * setVertexAugmentation) being released. "obj" is guaranteed not to be null. + * + * This may be called from the following ConnGraph methods: setVertexAugmentation, removeVertexAugmentation, and + * clear(). + */ + public void vertexAugmentationReleased(Object obj); +} diff --git a/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java b/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java index 545760892..885aee3a4 100644 --- a/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java +++ b/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java @@ -116,7 +116,10 @@ public class ConnGraph { private static final int MAX_VERTEX_COUNT = 1 << 30; /** The augmentation function for the graph, if any. */ - private final Augmentation augmentation; + final Augmentation augmentation; + + /** The AugmentationReleaseListener listening to release of ownership of augmentation objects, if any. */ + final AugmentationReleaseListener augmentationReleaseListener; /** * A map from each vertex in this graph to information about the vertex in this graph. If a vertex has no adjacent @@ -142,11 +145,93 @@ public class ConnGraph { /** Constructs a new ConnGraph with no augmentation. */ public ConnGraph() { augmentation = null; + augmentationReleaseListener = null; } - /** Constructs an augmented ConnGraph, using the specified function to combine augmentation values. */ + /** + * Constructs an augmented ConnGraph, using the specified function to combine augmentation values. See + * ConnGraph(MutatingAugmentation) regarding an alternative that may be more performant. + */ public ConnGraph(Augmentation augmentation) { this.augmentation = augmentation; + augmentationReleaseListener = null; + } + + /** + * Constructs an augmented ConnGraph, using the specified function to combine augmentation values. + * + * While ConnGraph(Augmentation) is a little more convenient and elegant, ConnGraph(MutatingAugmentation) should be + * more performant, provided that the Augmentation constructs a new object every time "combine" is called. This is + * because ConnGraph(MutatingAugmentation) is able to recycle (or "pool") augmentation objects that are no longer in + * use, thereby reducing the number of object allocations. + * + * However, not all Augmentations construct a new object every time "combine" is called. If the augmentation objects + * are Booleans, then an Augmentation would return a cached Boolean object each time "combine" is called (assuming + * it uses normal Java boxing conversions). So ConnGraph(Augmentation) is actually more efficient in the case of + * Booleans. In the case of Integers, Augmentation.combine may or may not return a cached Integer object, + * considering that the Integer class caches Integer objects for the range from -128 to 127. So + * ConnGraph(MutatingAugmentation) may or may not be more performant in the case of Integers. It is possible to + * combine the benefits of caching and object reuse by using the ConnGraph(Augmentation, + * AugmentationReleaseListener) constructor. + * + * Note that combined augmentation values returned by getComponentAugmentation may later be recycled and their + * contents changed. A return value of getComponentAugmentation should only be considered valid until the next time + * the graph is mutated or optimize() is called on it. + * + * Only combined augmentation objects are automatically recycled. Vertex augmentation objects passed to + * setVertexAugmentation are not. If you want to reduce the number of objects allocated for setting vertex + * augmentations, you can do so manually. For example: + * + * class IntWrapper { + * public int value; + * } + * + * class ThingThatUsesConnGraph { + * private IntWrapper augmentation; + * + * private void setVertexAugmentation(ConnGraph graph, ConnVertex vertex, int value) { + * if (augmentation == null) { + * augmentation = new IntWrapper(); + * } + * augmentation.value = value; + * augmentation = (IntWrapper)graph.setVertexAugmentation(vertex, augmentation); + * } + * + * ... + * } + */ + public ConnGraph(MutatingAugmentation mutatingAugmentation) { + AugmentationPool pool = new AugmentationPool(mutatingAugmentation); + augmentation = pool; + augmentationReleaseListener = pool; + } + + /** + * Constructs an augmented ConnGraph, using the specified function to combine augmentation values. + * + * The AugmentationReleaseListener can be used to track when an augmentation object is no longer stored in this + * graph and the object may be recycled. For example, if we are augmenting vertices with integers, we could create + * an IntWrapper class with an int field to store the augmentations. The first time Augmentation.combine is called, + * we would construct a new IntWrapper instance and return that. Later on, the AugmentationReleaseListener might + * tell us that the ConnGraph no longer needs the IntWrapper object. So in a subsequent call to + * Augmentation.combine, instead of constructing a new IntWrapper, we could set the old IntWrapper instance's int + * field and return that IntWrapper. + * + * Note that once an augmentation object is recycled, its old contents are lost. For example, if we are recycling + * combined augmentation objects, then normally, we should only consider a return value of getComponentAugmentation + * to be valid until the next time the graph is mutated or optimize() is called. After that point, the contents of + * the return value of getComponentAugmentation may have changed. + * + * ConnGraph(Augmentation, AugmentationReleaseListener) can be used to improve performance by minimizing the number + * of object allocations. Compared to ConnGraph(MutatingAugmentation), ConnGraph(Augmentation, + * AugmentationReleaseListener) offers more control, so it may be possible to improve performance using + * ConnGraph(Augmentation, AugmentationReleaseListener). For example, this constructor could be used to combine + * caching and object reuse techniques, as suggested in the comments for ConnGraph(MutatingAugmentation). + */ + // TODO: Is this useful enough to be worth exposing publicly? + public ConnGraph(Augmentation augmentation, AugmentationReleaseListener augmentationReleaseListener) { + this.augmentation = augmentation; + this.augmentationReleaseListener = augmentationReleaseListener; } /** Equivalent implementation is contractual. */ @@ -154,7 +239,7 @@ public class ConnGraph { if (augmentation == null) { throw new RuntimeException( "You may only call augmentation-related methods on ConnGraph if the graph is augmented, i.e. if an " + - "Augmentation was passed to the constructor"); + "Augmentation or MutatingAugmentation was passed to the constructor"); } } @@ -176,7 +261,7 @@ public class ConnGraph { } EulerTourVertex eulerTourVertex = new EulerTourVertex(); - EulerTourNode node = new EulerTourNode(eulerTourVertex, augmentation); + EulerTourNode node = new EulerTourNode(eulerTourVertex, this); eulerTourVertex.arbitraryVisit = node; node.left = EulerTourNode.LEAF; node.right = EulerTourNode.LEAF; @@ -486,7 +571,7 @@ public class ConnGraph { root = max.remove(); EulerTourNode[] splitRoots = root.split(vertex2.arbitraryVisit); root = splitRoots[1].concatenate(splitRoots[0]); - EulerTourNode newNode = new EulerTourNode(vertex2, root.augmentationFunc); + EulerTourNode newNode = new EulerTourNode(vertex2, root.graph); newNode.left = EulerTourNode.LEAF; newNode.right = EulerTourNode.LEAF; newNode.isRed = true; @@ -500,7 +585,7 @@ public class ConnGraph { EulerTourNode[] splitRoots = vertex1.arbitraryVisit.root().split(vertex1.arbitraryVisit); EulerTourNode before = splitRoots[0]; EulerTourNode after = splitRoots[1]; - EulerTourNode newNode = new EulerTourNode(vertex1, root.augmentationFunc); + EulerTourNode newNode = new EulerTourNode(vertex1, root.graph); before.concatenate(root, newNode).concatenate(after); return new EulerTourEdge(newNode, max); } @@ -946,6 +1031,12 @@ public class ConnGraph { } } } + + if (augmentationReleaseListener != null && oldAugmentation != null) { + // Note that oldAugmentation must be released after the calls to augment() earlier in the method, in order + // to ensure that we do not change its contents before returning it + augmentationReleaseListener.vertexAugmentationReleased(oldAugmentation); + } return oldAugmentation; } @@ -975,6 +1066,12 @@ public class ConnGraph { } } } + + if (augmentationReleaseListener != null && oldAugmentation != null) { + // Note that oldAugmentation must be released after the calls to augment() earlier in the method, in order + // to ensure that we do not change its contents before returning it + augmentationReleaseListener.vertexAugmentationReleased(oldAugmentation); + } return oldAugmentation; } @@ -1035,11 +1132,39 @@ public class ConnGraph { } } + /** + * Releases ownership of all of the augmentation values stored in this graph, by calling + * combinedAugmentationReleased and vertexAugmentationReleased on augmentationReleaseListener. This assumes that + * augmentationReleaseListener is non-null. + */ + private void releaseAugmentations() { + for (VertexInfo info : vertexInfo.values()) { + if (info.vertex.augmentation != null) { + augmentationReleaseListener.vertexAugmentationReleased(info.vertex.augmentation); + } + + EulerTourNode node = info.vertex.arbitraryVisit; + do { + if (node.ownsAugmentation) { + augmentationReleaseListener.combinedAugmentationReleased(node.augmentation); + } + node = node.successor(); + if (node == null) { + node = info.vertex.arbitraryVisit.root().min(); + } + } while (node.vertex.arbitraryVisit != node); + } + } + /** * Clears this graph, by removing all edges and vertices, and removing all augmentation information from the * vertices. */ public void clear() { + if (augmentationReleaseListener != null && augmentation != null) { + releaseAugmentations(); + } + // Note that we construct a new HashMap rather than calling vertexInfo.clear() in order to ensure a reduction in // space vertexInfo = new HashMap(); diff --git a/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java b/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java index 9d1f068f9..bca4f4044 100644 --- a/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java +++ b/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java @@ -28,16 +28,13 @@ class EulerTourNode extends RedBlackNode { */ public boolean hasForestEdge; - /** - * The combining function for combining user-provided augmentations. augmentationFunc is null if this node is not in - * the highest level. - */ - public final Augmentation augmentationFunc; + /** The graph this belongs to. "graph" is null instead if this node is not in the highest level. */ + public final ConnGraph graph; /** * The combined augmentation for the subtree rooted at this node. This is the result of combining the augmentation * values node.vertex.augmentation for all nodes "node" in the subtree rooted at this node for which - * node.vertex.arbitraryVisit == node, using augmentationFunc. This is null if hasAugmentation is false. + * node.vertex.arbitraryVisit == node, using graph.augmentation. This is null if hasAugmentation is false. */ public Object augmentation; @@ -48,9 +45,17 @@ class EulerTourNode extends RedBlackNode { */ public boolean hasAugmentation; - public EulerTourNode(EulerTourVertex vertex, Augmentation augmentationFunc) { + /** + * Whether this node "owns" "augmentation", with respect to graph.augmentationReleaseListener. A node owns a + * combined augmentation if the node obtained it by calling Augmentation.combine, as opposed to copying a reference + * to vertex.augmentation, left.augmentation, or right.augmentation. This is false if + * graph.augmentationReleaseListener or "augmentation" is null. + */ + public boolean ownsAugmentation; + + public EulerTourNode(EulerTourVertex vertex, ConnGraph graph) { this.vertex = vertex; - this.augmentationFunc = augmentationFunc; + this.graph = graph; } /** Like augment(), but only updates the augmentation fields hasGraphEdge and hasForestEdge. */ @@ -73,40 +78,71 @@ class EulerTourNode extends RedBlackNode { public boolean augment() { int newSize = left.size + right.size + 1; boolean augmentedFlags = augmentFlags(); - - Object newAugmentation = null; - boolean newHasAugmentation = false; - if (augmentationFunc != null) { - if (left.hasAugmentation) { - newAugmentation = left.augmentation; - newHasAugmentation = true; - } - if (vertex.hasAugmentation && vertex.arbitraryVisit == this) { - if (newHasAugmentation) { - newAugmentation = augmentationFunc.combine(newAugmentation, vertex.augmentation); - } else { - newAugmentation = vertex.augmentation; - newHasAugmentation = true; - } - } - if (right.hasAugmentation) { - if (newHasAugmentation) { - newAugmentation = augmentationFunc.combine(newAugmentation, right.augmentation); - } else { - newAugmentation = right.augmentation; - newHasAugmentation = true; - } + if (graph == null || graph.augmentation == null) { + if (newSize == size && !augmentedFlags) { + return false; + } else { + size = newSize; + return true; } } + AugmentationReleaseListener releaseListener = graph.augmentationReleaseListener; + Object newAugmentation = null; + int valueCount = 0; + if (left.hasAugmentation) { + newAugmentation = left.augmentation; + valueCount = 1; + } + if (vertex.hasAugmentation && vertex.arbitraryVisit == this) { + if (valueCount == 0) { + newAugmentation = vertex.augmentation; + } else { + newAugmentation = graph.augmentation.combine(newAugmentation, vertex.augmentation); + } + valueCount++; + } + if (right.hasAugmentation) { + if (valueCount == 0) { + newAugmentation = right.augmentation; + } else { + Object tempAugmentation = newAugmentation; + newAugmentation = graph.augmentation.combine(newAugmentation, right.augmentation); + if (valueCount >= 2 && releaseListener != null && tempAugmentation != null) { + releaseListener.combinedAugmentationReleased(tempAugmentation); + } + } + valueCount++; + } + + boolean newHasAugmentation = valueCount > 0; + boolean newOwnsAugmentation = valueCount >= 2 && releaseListener != null && newAugmentation != null; if (newSize == size && !augmentedFlags && hasAugmentation == newHasAugmentation && (newAugmentation != null ? newAugmentation.equals(augmentation) : augmentation == null)) { + if (newOwnsAugmentation) { + releaseListener.combinedAugmentationReleased(newAugmentation); + } return false; } else { + if (ownsAugmentation) { + releaseListener.combinedAugmentationReleased(augmentation); + } size = newSize; augmentation = newAugmentation; hasAugmentation = newHasAugmentation; + ownsAugmentation = newOwnsAugmentation; return true; } } + + @Override + public void removeWithoutGettingRoot() { + super.removeWithoutGettingRoot(); + if (ownsAugmentation) { + graph.augmentationReleaseListener.combinedAugmentationReleased(augmentation); + augmentation = null; + hasAugmentation = false; + ownsAugmentation = false; + } + } } diff --git a/src/main/java/com/github/btrekkie/connectivity/MutatingAugmentation.java b/src/main/java/com/github/btrekkie/connectivity/MutatingAugmentation.java new file mode 100644 index 000000000..0ec8b3586 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/MutatingAugmentation.java @@ -0,0 +1,51 @@ +package com.github.btrekkie.connectivity; + +/** + * A combining function for taking the augmentations associated with a set of ConnVertices and reducing them to a single + * result. MutatingAugmentation has a binary operation "combine" that takes two values, combines them using a function + * C, and stores the result in a mutable object. For example: + * + * class IntWrapper { + * public int value; + * } + * + * class Sum implements MutatingAugmentation { + * public void combine(Object value1, Object value2, Object result) { + * ((IntWrapper)result).value = ((IntWrapper)value1).value + ((IntWrapper)value2).value; + * } + * + * public Object newAugmentation() { + * return new IntWrapper(); + * } + * } + * + * Given vertices with augmentations A1, A2, A3, and A4, the combined result may be obtained by computing + * C(C(C(A1, A2), A3), A4). In order for an augmentation result to be meaningful, the combining function must be + * commutative, meaning C(x, y) is equivalent to C(y, x), and associative, meaning C(x, C(y, z)) is equivalent to + * C(C(x, y), z). + * + * If a ConnGraph represents a game map, then one example of an augmentation would be the amount of gold accessible from + * a certain location. Each vertex would be augmented with the amount of gold in that location, and the combining + * function would add the two amounts of gold passed in as arguments. Another example would be the strongest monster + * that can reach a particular location. Each vertex with at least one monster would be augmented with a reference to + * the strongest monster at that location, and the combining function would take the stronger of the two monsters passed + * in as arguments. A third example would be the number of locations accessible from a given vertex. Each vertex would + * be augmented with the number 1, and the combining function would add the two numbers of vertices passed in as + * arguments. + * + * ConnGraph treats two augmentation values X and Y as interchangeable if they are equal, as in + * X != null ? X.equals(Y) : Y == null. The same holds for two combined augmentation values, and for one combined + * augmentation value and one augmentation value. + * + * See the comments for ConnGraph. + */ +public interface MutatingAugmentation { + /** Computes the result of combining value1 and value2 into one, and stores it in "result". */ + public void combine(Object value1, Object value2, Object result); + + /** + * Constructs and returns a new augmentation object that may subsequently be passed to "combine". The initial + * contents of the object are ignored. + */ + public Object newAugmentation(); +} diff --git a/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java b/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java index 30feeae6a..66efe62fa 100644 --- a/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java +++ b/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java @@ -234,10 +234,11 @@ public class ConnGraphTest { assertFalse(graph.componentHasAugmentation(vertices.get(6).get(4))); } - /** Tests a graph with a hub-and-spokes subgraph and a clique subgraph. */ - @Test - public void testWheelAndClique() { - ConnGraph graph = new ConnGraph(SumAndMax.AUGMENTATION); + /** + * Tests the specified ConnGraph with a hub-and-spokes subgraph and a clique subgraph. The graph must be empty and + * be augmented with SumAndMax objects. + */ + private void checkWheelAndClique(ConnGraph graph) { Random random = new Random(6170); ConnVertex hub = new ConnVertex(random); List spokes1 = new ArrayList(10); @@ -352,6 +353,23 @@ public class ConnGraphTest { assertNull(graph.getVertexAugmentation(spokes2.get(8))); } + /** Tests a graph with a hub-and-spokes subgraph and a clique subgraph. */ + @Test + public void testWheelAndClique() { + checkWheelAndClique(new ConnGraph(SumAndMax.AUGMENTATION)); + + ConnGraph graph1 = new ConnGraph(SumAndMax.MUTATING_AUGMENTATION); + checkWheelAndClique(graph1); + graph1.clear(); + checkWheelAndClique(graph1); + + SumAndMaxPoolAndCache pool = new SumAndMaxPoolAndCache(); + ConnGraph graph2 = new ConnGraph(pool, pool); + checkWheelAndClique(graph2); + graph2.clear(); + checkWheelAndClique(graph2); + } + /** * Sets the matching between vertices.get(columnIndex) and vertices.get(columnIndex + 1) to the permutation * suggested by newPermutation. See the comments for the implementation of testPermutations(). @@ -467,10 +485,11 @@ public class ConnGraphTest { checkPermutation(graph, vertices, 7, new int[]{5, 2, 0, 6, 4, 7, 3, 1}); } - /** Tests a graph based on the United States. */ - @Test - public void testUnitedStates() { - ConnGraph graph = new ConnGraph(SumAndMax.AUGMENTATION); + /** + * Tests the specified ConnGraph with a graph based on the United States. The graph must be empty and be augmented + * with SumAndMax objects. + */ + private void checkUnitedStates(ConnGraph graph) { Random random = new Random(6170); ConnVertex alabama = new ConnVertex(random); assertNull(graph.setVertexAugmentation(alabama, new SumAndMax(7, 1819))); @@ -876,6 +895,13 @@ public class ConnGraphTest { assertNull(graph.getComponentAugmentation(arkansas)); } + /** Tests a graph based on the United States. */ + @Test + public void testUnitedStates() { + checkUnitedStates(new ConnGraph(SumAndMax.AUGMENTATION)); + checkUnitedStates(new ConnGraph(SumAndMax.MUTATING_AUGMENTATION)); + } + /** Tests ConnectivityGraph on the graph for a dodecahedron. */ @Test public void testDodecahedron() { diff --git a/src/test/java/com/github/btrekkie/connectivity/test/Reference.java b/src/test/java/com/github/btrekkie/connectivity/test/Reference.java new file mode 100644 index 000000000..7656981ed --- /dev/null +++ b/src/test/java/com/github/btrekkie/connectivity/test/Reference.java @@ -0,0 +1,28 @@ +package com.github.btrekkie.connectivity.test; + +/** + * Wraps a value using reference equality. In other words, two references are equal only if their values are the same + * object instance, as in ==. + * @param  The type of value. + */ +class Reference { + /** The value this wraps. */ + private final T value; + + public Reference(T value) { + this.value = value; + } + + public boolean equals(Object obj) { + if (!(obj instanceof Reference)) { + return false; + } + Reference reference = (Reference)obj; + return value == reference.value; + } + + @Override + public int hashCode() { + return System.identityHashCode(value); + } +} diff --git a/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java index 393c3599f..91d421a79 100644 --- a/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java +++ b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java @@ -1,6 +1,7 @@ package com.github.btrekkie.connectivity.test; import com.github.btrekkie.connectivity.Augmentation; +import com.github.btrekkie.connectivity.MutatingAugmentation; /** Stores two values: a sum and a maximum. Used for testing augmentation in ConnGraph. */ class SumAndMax { @@ -14,9 +15,31 @@ class SumAndMax { } }; - public final int sum; + /** A MutatingAugmentation that combines two SumAndMaxes into one. */ + public static final MutatingAugmentation MUTATING_AUGMENTATION = new MutatingAugmentation() { + @Override + public void combine(Object value1, Object value2, Object result) { + SumAndMax sumAndMax1 = (SumAndMax)value1; + SumAndMax sumAndMax2 = (SumAndMax)value2; + SumAndMax sumAndMaxResult = (SumAndMax)result; + sumAndMaxResult.sum = sumAndMax1.sum + sumAndMax2.sum; + sumAndMaxResult.max = Math.max(sumAndMax1.max, sumAndMax2.max); + } - public final int max; + @Override + public Object newAugmentation() { + return new SumAndMax(); + } + }; + + public int sum; + + public int max; + + /** Constructs a new SumAndMax with an arbitrary sum and max. */ + public SumAndMax() { + + } public SumAndMax(int sum, int max) { this.sum = sum; diff --git a/src/test/java/com/github/btrekkie/connectivity/test/SumAndMaxPoolAndCache.java b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMaxPoolAndCache.java new file mode 100644 index 000000000..102785d89 --- /dev/null +++ b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMaxPoolAndCache.java @@ -0,0 +1,121 @@ +package com.github.btrekkie.connectivity.test; + +import java.util.HashMap; +import java.util.Map; + +import com.github.btrekkie.connectivity.Augmentation; +import com.github.btrekkie.connectivity.AugmentationReleaseListener; + +/** + * An Augmentation implementation for SumAndMax that recycles (or "pools") SumAndMax instances and uses caching. This + * should be passed to the ConnGraph constructor as an AugmentationReleaseListener so that it can recycle unused + * SumAndMax objects. + */ +public class SumAndMaxPoolAndCache implements Augmentation, AugmentationReleaseListener { + /** The maximum number of unused SumAndMax objects to store in a pool. */ + private static final int CAPACITY = 3; + + /** + * The maximum "sum" and "max" values to store in the cache. We cache all possible SumAndMax instances where "sum" + * and "max" are both in the range [0, CACHE_SIZE). + */ + private static final int CACHE_SIZE = 10; + + /** + * A pool of SumAndMax instances we may reuse. The array has length CAPACITY, but only the first "size" elements + * contain reusable SumAndMax objects. The values in the array after the first "size" are unspecified. + */ + private SumAndMax[] pool = new SumAndMax[CAPACITY]; + + /** The number of reusable SumAndMax instances in the pool. */ + private int size; + + /** A CACHE_SIZE x CACHE_SIZE array of cached SumAndMax instances. cache[m][s] is equal to "new SumAndMax(s, m)". */ + private SumAndMax[][] cache; + + /** + * A map from each SumAndMax object that is a combined augmentation value that the graph has ownership of to the + * number of ownership claims the graph has on the object. + */ + private Map, Integer> ownershipCounts = new HashMap, Integer>(); + + public SumAndMaxPoolAndCache() { + cache = new SumAndMax[CACHE_SIZE][CACHE_SIZE]; + for (int max = 0; max < CACHE_SIZE; max++) { + for (int sum = 0; sum < CACHE_SIZE; sum++) { + cache[max][sum] = new SumAndMax(sum, max); + } + } + } + + @Override + public Object combine(Object value1, Object value2) { + SumAndMax sumAndMax1 = (SumAndMax)value1; + SumAndMax sumAndMax2 = (SumAndMax)value2; + int sum = sumAndMax1.sum + sumAndMax2.sum; + int max = Math.max(sumAndMax1.max, sumAndMax2.max); + + SumAndMax result; + if (sum >= 0 && sum < CACHE_SIZE && max >= 0 && max < CACHE_SIZE) { + result = cache[max][sum]; + } else if (size == 0) { + result = new SumAndMax(sum, max); + } else { + size--; + result = pool[size]; + result.sum = sum; + result.max = max; + } + + Reference resultReference = new Reference(result); + Integer count = ownershipCounts.get(resultReference); + ownershipCounts.put(resultReference, count != null ? count + 1 : 1); + return result; + } + + /** + * Shared implementation of combinedAugmentationReleased and vertexAugmentationReleased. + * @param obj The object that was released. + * @param isCombined Whether this is for a call to combinedAugmentationReleased. + */ + private void augmentationReleased(SumAndMax obj, boolean isCombined) { + if (obj == null) { + throw new IllegalArgumentException("Cannot release a null value"); + } + + if (isCombined) { + Reference reference = new Reference(obj); + Integer count = ownershipCounts.get(reference); + if (count == null) { + throw new RuntimeException( + "ConnGraph attempted to release a combined augmentation object it did not own"); + } + if (count <= 1) { + ownershipCounts.remove(reference); + } else { + ownershipCounts.put(reference, count - 1); + } + } + + if (obj.sum < 0 || obj.sum >= CACHE_SIZE || obj.max < 0 || obj.max >= CACHE_SIZE) { + if (size < CAPACITY) { + pool[size] = obj; + size++; + } else if (!isCombined) { + // We want to test reusing vertex augmentation objects, so we add the object to the pool even if the + // pool is already full + pool[CAPACITY - 1] = obj; + } + } + } + + @Override + public void combinedAugmentationReleased(Object obj) { + augmentationReleased((SumAndMax)obj, true); + } + + @Override + public void vertexAugmentationReleased(Object obj) { + augmentationReleased((SumAndMax)obj, false); + } +}