Skip to content

Commit 38b4c63

Browse files
authored
perf: internalize matched spec patterns during spec lookup (#14142)
This PR speeds up `mvcgen'` spec lookup by internalizing each matched spec pattern into the `SymM` share table on first lookup, so its instance arguments become pointer-equal to the program's and need not be re-internalized on every later lookup. `findSpecs` selects which spec applies by matching the program against the candidate patterns held in the discrimination tree. Those patterns are built at `@[spec]` registration and not internalized into the `mvcgen'`-ambient `SymM` share table, so their instance arguments, the `MonadStateOf`/`MonadReaderOf`/`MonadExceptOf` instances of `get`/`set`/`read`/`throw` and the like, are structurally equal to the program's but pointer-distinct, and the matcher re-internalizes them on each match. The first time a spec is tried, `findSpecs` internalizes its pattern and stores it back in the database. The internalized pattern is structurally unchanged, so it keys the discrimination tree at the same place and replaces the old entry in place. Later lookups detect the internalized pattern in O(1) with `Sym.isShared` (a membership query on the share table) and skip the work. The updated database is threaded through the `Scope`, which `solve` already carries. `Sym.isShared` and `Pattern.shareCommon` are defined in the spec-database module so the spec database and the backward-rule caches share one definition. Benchmark wall-clock for the `mvcgen'` step (`tests/bench/mvcgen/sym`, single-threaded, min of 5): | case | before | after | | --- | --- | --- | | ReaderState(1000) | 197 ms | 171 ms | | AddSubCancelDeep(700) | 178 ms | 144 ms | | GetThrowSet(600) | 126 ms | 119 ms | | MatchIota(150) | 36 ms | 33 ms | | AddSubCancel(1000) | 179 ms | 176 ms |
1 parent 48ea259 commit 38b4c63

3 files changed

Lines changed: 40 additions & 21 deletions

File tree

src/Lean/Elab/Tactic/Do/Internal/VCGen/Solve.lean

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,13 @@ and apply its cached backward rule. -/
294294
private def applySpec (scope : VCGen.Scope) (goal : MVarId) (target : Expr) (info : WPInfo) :
295295
VCGenM SolveResult := do
296296
trace[Elab.Tactic.Do.vcgen] "Applying a spec for {info.prog}. Excess args: {info.excessArgs}"
297-
match ← SpecTheorems.findSpecs scope.specs info.prog with
297+
-- Hand `findSpecs` the sole reference to the database so its in-place pattern internalization
298+
-- does not copy the discrimination tree, then thread the updated database back into the scope.
299+
let specs := scope.specs
300+
let scope := { scope with specs := default }
301+
let (result, specs) ← SpecTheorems.findSpecs specs info.prog
302+
let scope := { scope with specs }
303+
match result with
298304
| .error thms => stopOrErrorOnMissingSpec info.prog info.m thms
299305
| .ok thm =>
300306
trace[Elab.Tactic.Do.vcgen] "Spec for {info.prog}: {thm.proof}"

src/Lean/Elab/Tactic/Do/Internal/VCGen/SpecDB.lean

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ namespace Lean.Elab.Tactic.Do.Internal
2626

2727
open SpecAttr
2828

29+
/-- Returns `true` if `e` is already internalized into the current `SymM` share table, in which case
30+
`shareCommon e` returns `e` unchanged. -/
31+
public def _root_.Lean.Meta.Sym.isShared (e : Expr) : SymM Bool :=
32+
return (← get).share.set.contains { expr := e }
33+
34+
/--
35+
Internalizes the pattern's expressions into the current `SymM` share table.
36+
37+
A pattern is built in `MetaM`, outside the `SymM` thread whose table the targets are internalized
38+
into, so its closed subterms (the instance telescope of a `wp` application, for example) are
39+
structurally equal to but distinct from the targets'. Internalizing the pattern once makes those
40+
subterms pointer-equal to every target internalized afterwards, so matching them is `O(1)`.
41+
-/
42+
public def _root_.Lean.Meta.Sym.Pattern.shareCommon (p : Pattern) : SymM Pattern := do
43+
return { p with
44+
pattern := ← Sym.shareCommon p.pattern
45+
varTypes := ← p.varTypes.mapM Sym.shareCommon }
46+
2947
/--
3048
Instantiates a spec theorem's proof.
3149
@@ -110,19 +128,30 @@ Look up `SpecTheorem`s in the `@[spec]` database.
110128
Takes all specs that match the given program `e` and sorts by descending priority.
111129
-/
112130
public def SpecAttr.SpecTheorems.findSpecs (database : SpecTheorems) (e : Expr) :
113-
SymM (Except (Array SpecTheorem) SpecTheorem) := do
131+
SymM (Except (Array SpecTheorem) SpecTheorem × SpecTheorems) := do
114132
let e ← instantiateMVars e
115133
let e ← shareCommon e
116134
let candidates := Sym.getMatch database.specs e
117135
let candidates := candidates.filter fun spec => !database.erased.contains spec.proof
118136
if h : candidates.size = 1 then
119137
have : 0 < candidates.size := h ▸ Nat.zero_lt_one
120-
return .ok candidates[0]
138+
return (.ok candidates[0], database)
121139
-- It appears that insertion sort is *much* faster than qsort here.
122140
let candidates := candidates.insertionSort (·.priority > ·.priority)
141+
let mut database := database
123142
for spec in candidates do
143+
-- Match against the internalized pattern so its instance arguments are pointer-equal to the
144+
-- program's. A spec is internalized the first time it is tried and stored back in the database,
145+
-- so later lookups find it already in the share table and skip the work.
146+
let mut spec := spec
147+
unless ← isShared spec.pattern.pattern do
148+
spec := { spec with pattern := ← spec.pattern.shareCommon }
149+
-- Take sole ownership of the discrimination tree before inserting so the update is in place.
150+
let specs := database.specs
151+
database := { database with specs := default }
152+
database := { database with specs := Sym.insertPattern specs spec.pattern spec }
124153
let some _res ← spec.pattern.match? e | continue
125-
return .ok spec
126-
return .error candidates
154+
return (.ok spec, database)
155+
return (.error candidates, database)
127156

128157
end Lean.Elab.Tactic.Do.Internal

src/Lean/Elab/Tactic/Do/Internal/VCGen/Util.lean

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,6 @@ decomposes.
2626

2727
namespace Lean.Elab.Tactic.Do.Internal
2828

29-
/--
30-
Internalize a pattern's expressions into the current `SymM` share table.
31-
32-
A pattern is built in `MetaM`, outside the `SymM` thread whose table holds the targets it matches
33-
against, so its closed subterms (the instance telescope of a `wp` application, for example) are
34-
equal to but distinct from the targets'. Internalizing the pattern once makes those subterms
35-
pointer-equal to every target internalized afterwards, so matching them no longer re-internalizes
36-
the same constant terms on every application.
37-
38-
Designed for dot notation: `pattern.shareCommon`. Requires `open Lean.Elab.Tactic.Do.Internal`.
39-
-/
40-
public def _root_.Lean.Meta.Sym.Pattern.shareCommon (p : Pattern) : SymM Pattern := do
41-
return { p with
42-
pattern := ← Sym.shareCommon p.pattern
43-
varTypes := ← p.varTypes.mapM Sym.shareCommon }
44-
4529
/-- Internalize a backward rule's pattern into the current `SymM` share table. See
4630
`Pattern.shareCommon`. Designed for dot notation: `rule.shareCommon`. -/
4731
public def _root_.Lean.Meta.Sym.BackwardRule.shareCommon (rule : BackwardRule) : SymM BackwardRule :=

0 commit comments

Comments
 (0)