Skip to content

Improve reasoning about heaps #3931

Open
unp1 wants to merge 15 commits into
mainfrom
bubel/heap-simplification
Open

Improve reasoning about heaps #3931
unp1 wants to merge 15 commits into
mainfrom
bubel/heap-simplification

Conversation

@unp1

@unp1 unp1 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Intended Change

General performance improvements in particular for heap heavier programs. Its improvements contribute to finding smaller proofs.

This PR consoliates the previous performance PRs (cleaning out the last remnants of my AI stint ;-).

It provides SC and MC speed-up compared to current main. The focus is in particular on verification of heap-heavy programs with lots of heap manipulations

  • provides specialised heap simplification rules (except for one which is not supported by the taclet soundness framework all are proven sound and the proofs are include in the integration test suite for taclets)
  • improves theory handling in the quantifier heuristic and improved candidate cost assignment reducing proof size
  • improves parking-and-awakening of queued taclets by including the top level operator of the first (0-th) subterm if available (reduces amount of unnecessary worken and re-evaluated taclets further, even for a rather small heap heavy program the unnecessary re-evaluation of costs was reduced from 86 Mio to 5 Mio)
  • improves OSS performance by using lock-free caches
  • smaller performance and bug fixes

(M: Main, P: This PR, SC: Single Core, MT 4x: Multithreading 4x; measurements taken on a warm JVM; all times are proving times (automode))

Problem Time
(Main, SC)
Time
(PR, SC)
Time
(Main, MT 4x)
Time
(PR, MT 4x)
Nodes (Main) Nodes (PR)
SimpleLL.remove 16.8s 5.4s 6.4s 2.5s 59 011 31 782
symmArray 12s 6.6s 5.6s 2.9s 13 517 13 599
SaddleBack.search 11.4s 1.4s 4.1s 0.7s 31 614 5 145
gemplus.add 8.9s 6.8s 3.5s 2.9s 27 648 26 844
bike (Stipula case study) 42.5s 27.6s 14.6s 9.8s 61 894 55 009
bet (Stipula case study) 160.1s 83.3s 50.3s 27.9s 130 789 107 353
Total 251.8s 131s 84.5s 46.7s 324 473 239 732
RunAllFunProofs
(no forks,
does not include bike/bet)
350s 233.4s 209.5s 153s 873 065 771 036

Type of pull request

  • Refactoring (behaviour should not change or only minimally change)
  • There are changes to the (Java) code
  • There are changes to the taclet rule base

Ensuring quality

  • I added new test case(s) for new functionality:
  • I have tested the feature as follows: Several runs of runAll*Proofs (closes same amount of proofs)
  • I have checked that runtime performance has not deteriorated: it has improved for heap heavy proofs

Additional information and contact(s)

Parts done with AI tooling support.

The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.

@unp1 unp1 self-assigned this Jul 14, 2026
@unp1 unp1 added this to the v3.1.0 milestone Jul 14, 2026
@unp1
unp1 marked this pull request as ready for review July 14, 2026 19:31
@unp1
unp1 force-pushed the bubel/heap-simplification branch 6 times, most recently from 8febe7a to ab6eaa7 Compare July 21, 2026 09:22
@unp1 unp1 changed the title Improve heap simplification Improve reasoning about heaps Jul 21, 2026
@unp1
unp1 force-pushed the bubel/heap-simplification branch 2 times, most recently from 82d2622 to 5424fff Compare July 21, 2026 20:35
@unp1
unp1 enabled auto-merge July 21, 2026 20:35
@Drodt
Drodt self-requested a review July 22, 2026 08:19

@Drodt Drodt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some small things.

Comment thread key.core/src/main/java/de/uka/ilkd/key/java/ServiceCaches.java Outdated
Comment thread key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ElementaryUpdateHead.java Outdated
Comment thread key.core/src/main/resources/de/uka/ilkd/key/proof/rules/heapRules.key Outdated
@unp1
unp1 disabled auto-merge July 22, 2026 20:45
@unp1
unp1 force-pushed the bubel/heap-simplification branch 4 times, most recently from 76d70f4 to 7a6553a Compare July 22, 2026 23:33
unp1 added 8 commits July 23, 2026 07:37
…not supported by the taclet soundness po generator)
The rule-application queue returns mostly bases: candidates whose \assumes
formula is not yet present in the sequent. On large proofs about 97% of all
queue pops are such bases, and re-matching them almost never yields an
applicable rule, so the queue spends most of its time confirming that
nothing changed.

A base is now parked: set aside under wake keys that describe the formula it
waits for, and woken exactly in the round in which a matching formula is
added or changed. The keys are derived from the taclet's own compiled
matcher (PatternKeySource, computed from the same per-operator dispatch the
matching runs on), so what a key accepts cannot drift from what the matcher
accepts. Four key kinds refine precision, from the top operator family down
to the renaming-invariant hash of a concrete first argument; modality-headed
formulas stay on the coarse operator key so no hash ever walks a program.

The parked state lives in ParkedBases, one instance per goal, with
insertion-ordered containers throughout: operator hash codes differ between
runs, and a hash-ordered iteration would leak that difference into the wake
order and the proof. Dead bases are swept periodically to bound the memory
they hold. Waking too eagerly is harmless, the base fails to match and is
parked again; the class comment argues why a wake is never late. The proofs
found are statistics-identical to the unparked queue across the RunAllProofs
suite.

Wake keys speak one vocabulary, operator families
(JavaMatchPlanBuilder.matchFamilyOf): an operator stands for itself, a
parametric function instance for its base, a modality for its kind, and only
schematic operators name nothing. The family is position-relative in one
respect: the top of a candidate is observed after its leading updates are
removed (they are the find's update context), every other position as
written. An \assumes whose first argument is an update application, as in
\assumes(p({u}x) ==>), therefore parks under the update application family
and is woken exactly by the formulas whose first argument carries one;
without the update family such a base was stored under a key no formula
produces and was never woken. No shipped taclet has this shape, so the
RunAllProofs suite is rule-application-identical; the shape is reachable
through user-loaded taclets and is pinned by the self-wake test.

The parked counts are checked against the map in the dead-base sweep, which
walks the map anyway, and the wake order carries an assertion that it lists
every kind: a kind missing there would silently never wake its bases.

The wake cycle, the wake order, the release on waking and the sibling
locality of the per-head counts are pinned by unit tests over real taclet
applications.

with AI tooling support
The deterministic tie-break on equal-cost rule applications compares sequent
formulas by walking their operator names. On large sequents these walks
compare the same formulas over and over.

JTerm.nameHash() folds the operator names and arities of all subterms into a
cached hash and contains nothing else. Unlike hashCode() it excludes term
labels, whose hash can embed object identity, so the value is identical
across JVM runs, which every key in the proof-search order must be. The
formula comparison orders by this hash first and falls back to the name walk
only on equal hashes.

The comparators are package-visible, so the order property tests call them
directly.

with AI tooling support
An anonymous class cannot be annotated, so the locality lookup climbs to
the first named class for anonymous ones. Named classes still declare their
own locality, because a subclass's overrides may change what the cost
depends on.

with AI tooling support
Two bugfixes in TermImpl.

depth() compared each subterm's depth against the cache field, which is -1
while the value is being computed, so every comparison succeeded and the
result was the depth of the last subterm plus one instead of the maximum. A
regression test pins both argument orders.

containsJavaBlockRecursive() answered false for a modality whose program has
no statements. The modality is still part of the term and its JavaBlock
still carries meta information such as PositionInfos, so such terms must not
be treated as program-free (they are excluded from term caching for exactly
that reason). The marker now reports every modality; a regression test
covers the empty-program case.
Five independent changes on hot paths of the proof search, none of which
changes any proof.

OneStepSimplifier spreads its replace-known map over the renaming-invariant
term hash instead of a coarser key, so lookups hit fewer collisions.

CountOccurrencesFeature counts occurrences with a direct depth-pruned walk
of the sequent instead of instantiating the generic term-generator
machinery per query.

TypeConverter looks up theories by class instead of scanning a list, and
PredictCostProver fetches the integer theory once per prediction run
instead of per literal.

The duplicate-application veto rejects unequal terms by their cached
renaming-invariant hash before entering the label-insensitive comparison
walks; terms carrying a program are excluded, their hash would walk the
whole program.

ContainsExecutableCodeTermFeature answers false without walking the term
when the cached contains-program marker already says the term is
program-free (the query variant keeps walking, queries are function symbols
and not covered by the marker).

partially with AI tooling support
One is only termporart. But all of them have been proven to improve performance significantly (not just by a few percent)

with AI tooling support
unp1 added 7 commits July 23, 2026 07:37
Makes triggers theory aware and improves the quality of the produced instantiation candidates by rejecting unhelpful ones and improving precision on detecting helpful instantiations. It also assigns cost tiers to found candidates to distinguish candidates more likely to help from others.

The congruence orientation, the polarity walk and the minimal-cover search
are pinned by unit tests.

with AI tooling support
- The classic option is the safe fallback (which might go away once trust into the new ones is established)

The option is resolved once when a strategy is built, like every other
strategy option: the instantiation generator, the prediction feature and the
tie-break feature receive it at construction instead of reading the proof
settings per candidate, and the trigger-set cache records which supports
built an entry, so a lookup under the other setting rebuilds it.

with AI tooling support
…rprint

The veto that rejects a rule application already performed on the branch scans a bucket of applied applications that share the candidate's focus-term fingerprint. The fingerprint has to agree with the equality the bucket is probed with, which compares focus terms modulo irrelevant term labels: a duplicate whose focus differs only in a label (origin labels, for instance, carry where a term came from) must land in the candidate's bucket, or it escapes the veto.

A not so nice caveat is that we introduce another cached hashcode. The field will only be temporary and can be removed once the normal hashcode is label agnostic PR #3884.

The hash discriminator invariant of the tie-break, the agreement of its two
comparators and the label blindness of both ordering hashes are pinned by a
property test.

with AI tooling support
This reduces the locking overhead for single core and also multi-threaded proving significantly. Contrary to intuition the loss of cache sharing between goals (and workers) does not hurt performance, but improves it.
@unp1
unp1 force-pushed the bubel/heap-simplification branch from 7a6553a to 6d8a1af Compare July 23, 2026 05:38
@unp1
unp1 enabled auto-merge July 23, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants