From 9ecdca2128499e2d2ac09872d7aa284299335a9f Mon Sep 17 00:00:00 2001 From: "M.Schmidt" Date: Wed, 23 Sep 2026 16:54:10 +0200 Subject: [PATCH] Avoid a set per combineWith and cache TransitionFunctionImpl's hash Follow-up to #281. combineWith built a LinkedHashSet of both functions' state change statements on every call, only to throw it away again in the common case where the union is a single statement. It now checks whether the other function already has all of this function's statements (typically both have the same single one) and, if so, returns a function of the other's kind with the merged sequences; only a genuinely new combination builds an ImmutableSet, which CombinedTransitionFunctionImpl then takes over without copying. The result's statements, and the one getStateChangeStatement() returns, are the same as before. hashCode() hashed the whole sequence multimap on every call, while the weights are hashed over and over as keys of the automata's maps. The functions are immutable, so the hash is now computed once and cached. With compressed oops the extra int fits in the padding of a plain TransitionFunctionImpl. --- .../CombinedTransitionFunctionImpl.java | 8 ++-- .../typestate/TransitionFunctionImpl.java | 43 +++++++++++++------ .../typestate/TransitionFunctionImplTest.java | 3 ++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/idealPDS/src/main/java/typestate/CombinedTransitionFunctionImpl.java b/idealPDS/src/main/java/typestate/CombinedTransitionFunctionImpl.java index 3c8d3568..5413eb91 100644 --- a/idealPDS/src/main/java/typestate/CombinedTransitionFunctionImpl.java +++ b/idealPDS/src/main/java/typestate/CombinedTransitionFunctionImpl.java @@ -23,9 +23,9 @@ import typestate.finiteautomata.Transition; /** - * The result of combining transition functions with different state change statements. It keeps - * all of them so that {@link TransitionFunctionImpl#combineWith} stays commutative, while the - * common single-statement case in {@link TransitionFunctionImpl} does not pay for a set. + * The result of combining transition functions with different state change statements. It keeps all + * of them so that {@link TransitionFunctionImpl#combineWith} stays commutative, while the common + * single-statement case in {@link TransitionFunctionImpl} does not pay for a set. */ final class CombinedTransitionFunctionImpl extends TransitionFunctionImpl { @@ -71,7 +71,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + int computeHashCode() { return Objects.hash(getStateChangeSequences(), stateChangeStatements); } } diff --git a/idealPDS/src/main/java/typestate/TransitionFunctionImpl.java b/idealPDS/src/main/java/typestate/TransitionFunctionImpl.java index 5b3ca15e..2ab2a01e 100644 --- a/idealPDS/src/main/java/typestate/TransitionFunctionImpl.java +++ b/idealPDS/src/main/java/typestate/TransitionFunctionImpl.java @@ -20,11 +20,11 @@ import boomerang.scope.Statement; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -40,6 +40,12 @@ public class TransitionFunctionImpl implements TransitionFunction { @NonNull private final Multimap stateChangeSequences; @NonNull private final Statement stateChangeStatement; + /** + * Cached {@link #hashCode()}, 0 until computed. Hashing the sequences is expensive, and weights + * are hashed over and over as keys of the automata's maps. + */ + private int hashCode; + public TransitionFunctionImpl( @NonNull Transition transition, @NonNull Statement stateChangeStatement) { this.stateChangeSequences = @@ -70,11 +76,10 @@ public TransitionFunctionImpl( } /** - * Returns a transition function with the given sequences and the state change statements of - * this function. + * Returns a transition function with the given sequences and the state change statements of this + * function. */ - @NonNull - TransitionFunctionImpl withStateChangeSequences( + @NonNull TransitionFunctionImpl withStateChangeSequences( @NonNull Multimap transitionStatementSequences) { return new TransitionFunctionImpl(transitionStatementSequences, stateChangeStatement); } @@ -94,8 +99,7 @@ public Statement getStateChangeStatement() { * statement returns just that one; a combination of functions returns the statements of all of * them (see {@link CombinedTransitionFunctionImpl}). */ - @NonNull - Set getStateChangeStatements() { + @NonNull Set getStateChangeStatements() { return Collections.singleton(stateChangeStatement); } @@ -181,13 +185,16 @@ public Weight combineWith(@NonNull Weight other) { // combineWith has to be commutative, so the result keeps the state change statements of both // functions. Keeping only one of them makes PostStar.update and // WeightedPAutomaton.addWeightForTransition replace each other's weights forever. - Set mergedStateChangeStatements = - new LinkedHashSet<>(func.getStateChangeStatements()); - mergedStateChangeStatements.addAll(getStateChangeStatements()); - if (mergedStateChangeStatements.size() == 1) { - return new TransitionFunctionImpl(sequences, func.getStateChangeStatement()); + // The common case is that func already has all of this function's statements (typically both + // have the same single one), so the result has func's statements and no set is built. + Set ownStatements = getStateChangeStatements(); + Set otherStatements = func.getStateChangeStatements(); + if (otherStatements.containsAll(ownStatements)) { + return func.withStateChangeSequences(sequences); } - return new CombinedTransitionFunctionImpl(sequences, mergedStateChangeStatements); + return new CombinedTransitionFunctionImpl( + sequences, + ImmutableSet.builder().addAll(otherStatements).addAll(ownStatements).build()); } @Override @@ -206,6 +213,16 @@ public boolean equals(Object o) { @Override public int hashCode() { + int h = hashCode; + if (h == 0) { + h = computeHashCode(); + hashCode = h; + } + return h; + } + + /** The uncached {@link #hashCode()}. */ + int computeHashCode() { return Objects.hash(stateChangeSequences, stateChangeStatement); } } diff --git a/idealPDS/src/test/java/typestate/TransitionFunctionImplTest.java b/idealPDS/src/test/java/typestate/TransitionFunctionImplTest.java index b4578b52..f19a4ec8 100644 --- a/idealPDS/src/test/java/typestate/TransitionFunctionImplTest.java +++ b/idealPDS/src/test/java/typestate/TransitionFunctionImplTest.java @@ -80,6 +80,9 @@ public void testCombineWithIdempotentAndCommutativeForMoreStatements() { Weight firstSecond = first.combineWith(second); assertInstanceOf(CombinedTransitionFunctionImpl.class, firstSecond); assertEquals(firstSecond, firstSecond.combineWith(first)); + // the other direction takes the shortcut for a function that already has all statements + assertEquals(firstSecond, first.combineWith(firstSecond)); + assertEquals(firstSecond.hashCode(), first.combineWith(firstSecond).hashCode()); assertEquals(firstSecond, firstSecond.combineWith(firstSecond)); Weight left = firstSecond.combineWith(third);