Repository navigation
Conversation
There was a problem hiding this comment.
Hi thanks,
Performance
I am currently running the complete benchmark set. One observation is that the replacement seems to have introduced somewhere a performance regressions:
The benchmark family shows for straightline_1000/4000/8000/10000: +0.7% / +6.4% / +9.5% / +14.1% compared to main
(problem is that for instance "StatementBlock.getChildAt/getChildCount" can no longer be inlined by the JIT, enforcing the type in StatementBlock to ImmutableListArray pushes the 14% to 7%, but there seems to be somewhere else a similar problem).
The common type hides necessary information for the JVM to optimize by inlining. Maybe we should allow classes where it is known the number of elements are fixed to use as a private member ImmutableListArray, but they have to handout only the generic type? I think the main reason behind it is that the abstraction layer (the common ImmList type) hides sometimes too much. Arrays have different characteristics than lists and to get all advantages from them, certain sides might need to be able to distinguish them and/or fix the kind of datatype they use.
There was a problem hiding this comment.
Correctness
I seem not to be able to push to this branch, hence here:
Adding
Function h = new JFunction(new Name("h"), JavaDLTheory.FORMULA, sort1, sort3);
@Test
public void testEqualsOfDifferentBuildTerms() {
JTerm viaArray = tf.createTerm(h, ImmutableList.of(tb.var(x), tb.var(y)), null, null);
JTerm viaList =
tf.createTerm(h, ImmutableList.fromList(List.of(tb.var(x), tb.var(y))), null, null);
assertEquals(viaArray.subs(), viaList.subs());
assertEquals(viaArray, viaList);
}
@Test
public void mergeRuleRebuildEqualsInput() {
JTerm term = tf.createTerm(h, tb.var(x), tb.var(y));
JTerm rebuilt =
MergeRuleUtils.substConstantsByFreshVars(term, new HashMap<>(),
TacletForTests.services());
assertEquals(term, rebuilt);
}to TestTerm will let the tests fail also they should succeed/ The fix is
@Override
public int hashCode() {
return ImmutableList.hash(this);
}instead of
@Override
public int hashCode() {
return Arrays.hashCode(data);
}in ImmutableListArray.
Independent of this PR that 'bug' exists already on main and just did not bite us yet.
There was a problem hiding this comment.
ImmutableList.get should throw an IndexOutOfBoundsException (that is also what the comment says) instead of "NoSuchElementException". At the moment this leads to non-caught cases e.g. in Translator.java (1402 to 1409) and a suboptimal error message for e.g. \singleton(o) in a method spec 'o' being a parameter other call side sdepend also on an IndexOutOfBounds (e.g. Goal)
public class SingletonError {
/*@ public normal_behavior
@ assignable \singleton(o);
@*/
public void m(Object o) {
}
}
The performance impact is confirmed. A "fix: would be to use these methods in the list factory ImmutableList static <T extends @Nullable Object> ImmutableList<T> of(T e1) {
return new ImmutableListArray<>((T[]) new Object[] { e1 });
}
static <T extends @Nullable Object> ImmutableList<T> of(T... es) {
if (es.length == 0) {
return of();
}
return new ImmutableListArray<>(es.clone());
}This actually turns it even in a slight improvement compared to main (except for straightline_10000). But I am not really a fan of it, as it has some disadvantages (slight memory increase for small lists and building a list via prepend/append starting from singleton lists leads to a worse structures as they do not start from a cons cell, does not happen at the moment, all lists constructed via prep/app are constructed starting from a NIL list). The effect it the following: the JVM Hotspot can now deduce from the only setting site in the constructor of TermImpl, StatementBlock etc. that the subterm/children list is either NIL or an ImmListArray and it will inline the calls (it does not do so if there are more than two options, like before where it could have been NIL, Cons or ImmListArray). A compromise (instead of allowing the use of type ImmListArray) might be to have a special "of" methods that only returns one (or at most two) implementation variants: e.g., ofFixedSize(...) and to use that at places where it is performance critical? To counter the performance impact that would be: TermImpl#subs, TermImpl#boundVars, LabeledTermImpl#labels, AbstractSortedOperator#argSorts, AbstractSortedOperator#whereToBind, StatementBlock#body, and several other Java AST classes that are iterated over during matching. Also the uses in the Strategy package need a closer look as they iterate millions of times and would pay the price if the calls could not get inlined. Nevertheless, I'll report the current performance numbers of the benchmark runs tomorrow when they finish. |
|
All three review items addressed; new commits pushed to 1. hashCode/equals contract (correctness)
2.
|
|
Mhm... it seems that there is still performance regression: ~38m is the baseline for runallproofs. |
A while ago I added the StipuLa case studies to heave large proofs in our collection, so the overall time is fine. I run the current version against the benchmarks and the performance regression on proof search alone is still around 8%. I'll run today and tomorrow a few target tests which call sites are the culprits and then let's see how to adress that. |
|
Not sure if we should really replace the immutable arrays (in the benchmarks, the conversions are increasingly fixed for more and more cases and we still have a slow-down of 10% and more) in the core prover classes. Problem is that the whole solution gets brittle, just at one construction site of a term, program element etc. a list with a cons cell might be created and the whole inlining breaks down. The knowledge where to take care would now be hidden away and no longer explicitly communicated by the type of the collection. |
|
Just for discussion on Friday, attached a patch (pr4001-array.patch) against 9f25f0c that uses ImmListArray directly, but keeps the type close to its chest (we still get rid of ImmutableArray, but make ImmListArray public, creation happens still only via ImmutableList). Regressions on main (4 Runden mit wechselnder Anordnung A/B/B/A/A/B/B/A ) are now between +4% for some proofs and -3% for others (with a slight direction to a bit slower in general), so it more or less identical in average and not major for any single proof (more within run-to-run differences). |
The array-backed implementation computed its hash code with Arrays.hashCode whereas all list-backed implementations (ImmutableSLList) use hash = 31 * hash + element. Equal lists therefore could have different hash codes, breaking the equals/hashCode contract and leading to inconsistent behavior in hash-based collections. After this PR equal contents can be built both via ImmutableList.of (array backed) and ImmutableList.fromList (list backed), which exposed the pre-existing bug. Use ImmutableList.hash instead. Co-Authored-By: OpenCode (KIT Medium)
The ImmutableList contract declares IndexOutOfBoundsException for get, but Cons.get(idx) ran head() on the empty tail and NIL.get threw NoSuchElementException for every index. This made callers that rely on IndexOutOfBoundsException (e.g. Translator handling of \singleton) miss the case and produced poor error messages. Add explicit bounds checks. Co-Authored-By: OpenCode (KIT Medium)
Contrary to of(T...), the new factory always returns an ImmutableListArray (or NIL for an empty sequence) instead of a cons cell for single elements. HotSpot cannot inline calls on fields whose possible runtime types are NIL, a cons cell and an ImmutableListArray; restricting hot construction sites (term subterm lists, operator argument sorts, Java AST children lists) to this factory leaves only two variants and enables inlining. Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
createSubtermArray is the single construction site for the subterm lists of all terms; restricting it plus the single-element label/sub variants to the array-backed implementation lets the JIT inline subs()/getLabels() access. Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
StatementBlock.getChildAt/getChildCount are hot during program matching; restricting the body list to NIL or ImmutableListArray allows the JIT to inline them. Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Co-Authored-By: OpenCode (KIT Medium)
Covers the cases reported in the PR review: terms built from array-backed vs list-backed subterm lists must be equal with equal hash codes, and rebuilding a term via MergeRuleUtils.substConstantsByFreshVars must preserve equality. Co-Authored-By: OpenCode (KIT Medium)
9f25f0c to
5a889b7
Compare
|
The branch has been rebased onto the latest
One note from the rebase: Happy to re-discuss any of the points — please re-review when convenient. |
Related Issue
Intended Change
Removes the deprecated
ImmutableArraycollection class (org.key_project.util.collection) in favour ofImmutableList, whose array-backed implementationImmutableListArray(constructed viaImmutableList#of(Array)) provides a better interface. The deprecated class itself is deleted in the final commit.All usages were migrated:
Term/JTerm(subs(),boundVars(),varsBoundHere(),getLabels(),argSorts(), …) now returnImmutableListnew ImmutableArray<>(…)→ImmutableList.of(…)(empty / single element / varargs array)new ImmutableArray<>(collection)→ImmutableList.fromList(…)(arr, lo, hi)→ImmutableList.of(Arrays.copyOfRange(arr, lo, hi))arraycopy(...)call sites →toArray(...)/ element loops.toImmutableList()dropped (values are already immutable lists)OpReplacer.replace(ImmutableList<QuantifiableVariable>)renamed toreplaceQuantifiableVariables(erasure clash that was previously hidden by the different class)Plan
The migration is complete; one commit per migrated class.
Type of pull request
Ensuring quality
key.util,key.ncore,key.ncore.calculus,key.ncore.matcher,key.core(main + tests),key.core.infflow,key.core.testgen,keyext.isabelletranslation); the full test suite runs on CI.Additional information and contact(s)
The migration is mostly mechanical; the few non-mechanical decisions are listed under Intended Change. The PR is intentionally opened in draft mode so that CI can run before it is considered merge-ready.
The contributions within this pull request are licensed under GPLv2, or (at your option) any later version, for inclusion in KeY.