Skip to content

Commit 8b0a880

Browse files
Khaclaude
andcommitted
perf: key type class resolution cache by query only
Drop `synthPendingDepth` from the type class resolution cache key: an entry now pairs an optional shared result (bounded by its relative `synthPending` activity depth) with an optional depth-exact result (from syntheses that hit the `maxSynthPendingDepth` give-up). Each query thus performs a single cache lookup, and both kinds of result for the same query can coexist without evicting each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3fceb1d commit 8b0a880

2 files changed

Lines changed: 104 additions & 96 deletions

File tree

src/Lean/Meta/Basic.lean

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -326,22 +326,8 @@ instance : Hashable InfoCacheKey where
326326

327327
-- Remark: we don't need to store `Config.toKey` because typeclass resolution uses a fixed configuration.
328328
structure SynthInstanceCacheKey where
329-
localInsts : LocalInstances
330-
type : Expr
331-
/--
332-
Value of `synthPendingDepth` when the instance was synthesized or failed to be
333-
synthesized, or `none` if the result can be reused at other depths.
334-
335-
`synthPendingDepth` can influence the result of a query because `synthPending` gives up
336-
when `synthPendingDepth > maxSynthPendingDepth`, so a result may not be reused at a
337-
different depth (see issue #2522). However, this is the *only* way the depth can influence
338-
a query. Thus, if no `synthPending` invocation gave up while synthesizing an instance, the
339-
result is valid at every depth at which the guard still cannot trigger, and is stored with
340-
`synthPendingDepth := none` together with a validity bound in the cache entry (see
341-
`SynthInstanceCacheEntry.relSynthPendingDepth`). Only results whose synthesis hit the
342-
give-up threshold remain keyed by their exact depth.
343-
-/
344-
synthPendingDepth : Option Nat
329+
localInsts : LocalInstances
330+
type : Expr
345331
deriving Hashable, BEq
346332

347333
/-- Resulting type for `abstractMVars` -/
@@ -354,18 +340,32 @@ structure AbstractMVarsResult where
354340
def AbstractMVarsResult.numMVars (r : AbstractMVarsResult) : Nat :=
355341
r.mvars.size
356342

357-
/-- Entry of the type class resolution cache. -/
343+
/--
344+
Entry of the type class resolution cache. In both fields, a `none`
345+
`AbstractMVarsResult` records that the synthesis failed.
346+
347+
`synthPendingDepth` can influence the result of a query because `synthPending` gives up
348+
when `synthPendingDepth > maxSynthPendingDepth` (see issue #2522) — and this is the *only*
349+
way the depth can influence a query. Each field's validity condition ensures that every
350+
`synthPending` decision reached during the stored synthesis comes out the same way for the
351+
reusing query, so the synthesis would behave identically at its depth.
352+
-/
358353
structure SynthInstanceCacheEntry where
359354
/--
360-
Maximum `synthPendingDepth` at which a `synthPending` decision was reached during the
361-
synthesis, relative to the query's own `synthPendingDepth`, or `none` if no such decision
362-
was reached. An entry stored under `synthPendingDepth := none` may be reused by a query at
363-
depth `d` iff `relSynthPendingDepth` is `none` or `d + relSynthPendingDepth` does not
364-
exceed `maxSynthPendingDepth`: under that bound no `synthPending` invocation can reach the
365-
give-up threshold, so the synthesis behaves identically at depth `d`.
355+
Result shared across `synthPendingDepth` values, together with the maximum
356+
`synthPendingDepth` at which a `synthPending` decision was reached during its synthesis,
357+
relative to the query's own depth (`none` if no decision was reached). It may be reused
358+
by a query at depth `d` iff the bound is `none` or `d + bound` does not exceed
359+
`maxSynthPendingDepth`: under that condition, no `synthPending` invocation can reach the
360+
give-up threshold.
361+
-/
362+
shared? : Option (Option Nat × Option AbstractMVarsResult) := none
363+
/--
364+
Result of a synthesis during which some `synthPending` invocation gave up because of
365+
`maxSynthPendingDepth`, together with the exact `synthPendingDepth` it was computed at;
366+
it may only be reused at that exact depth.
366367
-/
367-
relSynthPendingDepth : Option Nat := none
368-
result : Option AbstractMVarsResult
368+
exact? : Option (Nat × Option AbstractMVarsResult) := none
369369

370370
abbrev SynthInstanceCache := PersistentHashMap SynthInstanceCacheKey SynthInstanceCacheEntry
371371

@@ -485,7 +485,7 @@ register_builtin_option maxSynthPendingDepth : Nat := {
485485
/--
486486
Accumulated `synthPending` decisions of a type class query, used by the type class
487487
resolution cache to bound the `synthPendingDepth` values at which the result may be reused.
488-
See `SynthInstanceCacheEntry.relSynthPendingDepth`.
488+
See `SynthInstanceCacheEntry`.
489489
-/
490490
structure SynthPendingActivity where
491491
/-- Maximum `synthPendingDepth` at which a `synthPending` decision was reached. -/
@@ -537,7 +537,7 @@ structure Context where
537537
When set, the reference accumulates the `synthPending` decisions reached during the
538538
current type class query, i.e. behavior that may depend on `synthPendingDepth`. The type
539539
class resolution cache uses this to decide at which depths a result may be reused; see
540-
`SynthInstanceCacheKey.synthPendingDepth` and `SynthInstanceCacheEntry.relSynthPendingDepth`.
540+
`SynthInstanceCacheEntry`.
541541
The reference is intentionally not part of the backtrackable state: a `synthPending`
542542
invocation in a discarded search branch still influenced the search outcome.
543543
-/

src/Lean/Meta/SynthInstance.lean

Lines changed: 77 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -856,11 +856,24 @@ private def applyCachedAbstractResult? (type : Expr) (abstResult? : Option Abstr
856856
applyAbstractResult? type abstResult?
857857

858858
/-- Helper function for caching synthesized type class instances. -/
859-
private def cacheResult (cacheKey : SynthInstanceCacheKey) (relSynthPendingDepth : Option Nat)
860-
(kind : PreprocessKind) (abstResult? : Option AbstractMVarsResult) (result? : Option Expr) : MetaM Unit := do
859+
private def cacheResult (cacheKey : SynthInstanceCacheKey) (synthPendingDepth : Nat)
860+
(activity : SynthPendingActivity) (kind : PreprocessKind)
861+
(abstResult? : Option AbstractMVarsResult) (result? : Option Expr) : MetaM Unit := do
861862
let insert (result : Option AbstractMVarsResult) : MetaM Unit :=
862-
modify fun s => { s with
863-
cache.synthInstance := s.cache.synthInstance.insert cacheKey { relSynthPendingDepth, result } }
863+
modify fun s => { s with cache.synthInstance :=
864+
-- Merge into the existing entry (if any) so that shared and depth-exact results for
865+
-- the same key can coexist. Re-reading the entry here is required for correctness:
866+
-- nested queries during the search may have stored results for the same key.
867+
let entry := s.cache.synthInstance.find? cacheKey |>.getD {}
868+
let entry :=
869+
if activity.guardHit then
870+
-- The synthesis hit the `maxSynthPendingDepth` give-up, so the result is only
871+
-- valid at the exact depth; all others are shared, bounded by their relative
872+
-- activity depth.
873+
{ entry with exact? := some (synthPendingDepth, result) }
874+
else
875+
{ entry with shared? := some (activity.maxDepth.map (· - synthPendingDepth), result) }
876+
s.cache.synthInstance.insert cacheKey entry }
864877
match abstResult? with
865878
| none => insert none
866879
| some abstResult =>
@@ -874,6 +887,10 @@ private def cacheResult (cacheKey : SynthInstanceCacheKey) (relSynthPendingDepth
874887
else
875888
insert (some abstResult)
876889

890+
/-- Trace-message suffix showing a nonzero `synthPendingDepth`. -/
891+
private def depthSuffix (synthPendingDepth : Nat) : MessageData :=
892+
if synthPendingDepth == 0 then m!"" else m!" (synthPendingDepth := {synthPendingDepth})"
893+
877894
def synthInstanceCore? (type : Expr) (maxResultSize? : Option Nat := none) : MetaM (Option Expr) := do
878895
let opts ← getOptions
879896
let maxResultSize := maxResultSize?.getD (synthInstance.maxSize.get opts)
@@ -886,8 +903,6 @@ def synthInstanceCore? (type : Expr) (maxResultSize? : Option Nat := none) : Met
886903
let type ← instantiateMVars type
887904
let { type, cacheKeyType, kind } ← preprocess type
888905
let synthPendingDepth := (← read).synthPendingDepth
889-
let depthSuffix : MessageData :=
890-
if synthPendingDepth == 0 then m!"" else m!" (synthPendingDepth := {synthPendingDepth})"
891906
-- Fold `synthPending` activity into the enclosing query's accumulator, if any.
892907
let foldActivity (activity : SynthPendingActivity) : MetaM Unit := do
893908
if activity.maxDepth.isSome || activity.guardHit then
@@ -898,71 +913,64 @@ def synthInstanceCore? (type : Expr) (maxResultSize? : Option Nat := none) : Met
898913
| some d, none | none, some d => some d
899914
| none, none => none
900915
guardHit := a.guardHit || activity.guardHit }
901-
let applyCached (entry : SynthInstanceCacheEntry) : MetaM (Option Expr) := do
902-
trace[Meta.synthInstance.cache] "cached{depthSuffix}: {type}"
903-
let result? ← applyCachedAbstractResult? type entry.result
916+
let applyCached (abstResult? : Option AbstractMVarsResult) : MetaM (Option Expr) := do
917+
trace[Meta.synthInstance.cache] "cached{depthSuffix synthPendingDepth}: {type}"
918+
let result? ← applyCachedAbstractResult? type abstResult?
904919
trace[Meta.synthInstance] "result {result?} (cached)"
905920
return result?
906-
let sharedKey : SynthInstanceCacheKey := { localInsts, type := cacheKeyType, synthPendingDepth := none }
907-
let depthKey := { sharedKey with synthPendingDepth := some synthPendingDepth }
908-
let maxSynthPending := maxSynthPendingDepth.get (← getOptions)
909-
let sharedEntry? :=
910-
match (← get).cache.synthInstance.find? sharedKey with
911-
| some entry =>
912-
match entry.relSynthPendingDepth with
913-
| none => some entry
914-
| some rel =>
915-
-- Valid iff no `synthPending` invocation can reach the give-up threshold at the
916-
-- current depth; see `SynthInstanceCacheEntry.relSynthPendingDepth`.
917-
if synthPendingDepth + rel ≤ maxSynthPending then some entry else none
918-
| none => none
919-
if let some entry := sharedEntry? then
920-
-- Reusing the entry re-enacts its `synthPending` decisions at the current depth.
921-
foldActivity { maxDepth := entry.relSynthPendingDepth.map (synthPendingDepth + ·) }
922-
applyCached entry
923-
else if let some entry := (← get).cache.synthInstance.find? depthKey then
924-
-- The entry's synthesis hit the `maxSynthPendingDepth` give-up, so reusing it keeps
925-
-- the enclosing query depth-exact as well.
926-
foldActivity { maxDepth := some synthPendingDepth, guardHit := true }
927-
applyCached entry
928-
else
929-
trace[Meta.synthInstance.cache] "new{depthSuffix}: {type}"
930-
let activityRef ← IO.mkRef {}
931-
let abstResult? ← withReader (fun ctx => { ctx with synthPendingActivityRef? := some activityRef }) do
932-
withNewMCtxDepth (allowLevelAssignments := true) do
933-
match kind with
934-
| .noMVars =>
935-
/-
936-
**Note**: The expensive `preprocessOutParam` step is morally **not** needed here because
937-
the output params should be uniquely determined by the input params. During type class
938-
resolution, definitional equality only unfolds `[reducible]` and `[instance_reducible]`
939-
declarations. This is a contract with our users to ensure performance is reasonable.
940-
However, the same `OrderDual` declaration that creates problems for `assignOutParams`
941-
also prevents us from using this optimization. As an example, suppose we are trying to
942-
synthesize
943-
```
944-
FunLike F (OrderDual α) (OrderDual β)
945-
```
946-
where the last two arguments of `FunLike` are output parameters. This term has no
947-
metavariables, and it seems natural to skip `preprocessOutParam`, which would replace
948-
the last two arguments with metavariables. However, if we don't replace them,
949-
TC resolution fails because it cannot unfold `OrderDual` since it is semireducible.
950-
951-
**Note**: We should remove `preprocessOutParam` from the following line as soon as
952-
Mathlib refactors `OrderDual`.
953-
-/
954-
SynthInstance.main (← preprocessOutParam type) maxResultSize
955-
| .mvarsNoOutputParams => SynthInstance.main type maxResultSize
956-
| .mvarsOutputParams => SynthInstance.main (← preprocessOutParam type) maxResultSize
957-
let result? ← applyAbstractResult? type abstResult?
958-
trace[Meta.synthInstance] "result {result?}"
959-
let activity ← activityRef.get
960-
foldActivity activity
961-
-- Results whose synthesis hit the `maxSynthPendingDepth` give-up are only valid at
962-
-- the exact depth; all others are shared, bounded by their relative activity depth.
963-
let key := if activity.guardHit then depthKey else sharedKey
964-
cacheResult key (activity.maxDepth.map (· - synthPendingDepth)) kind abstResult? result?
965-
return result?
921+
let key : SynthInstanceCacheKey := { localInsts, type := cacheKeyType }
922+
if let some entry := (← get).cache.synthInstance.find? key then
923+
if let some (rel, abstResult?) := entry.shared? then
924+
-- Valid iff no `synthPending` invocation can reach the give-up threshold at the
925+
-- current depth; see `SynthInstanceCacheEntry.shared?`.
926+
let valid ←
927+
match rel with
928+
| none => pure true
929+
| some rel => do pure (synthPendingDepth + rel ≤ maxSynthPendingDepth.get (← getOptions))
930+
if valid then
931+
-- Reusing the result re-enacts its `synthPending` decisions at the current depth.
932+
foldActivity { maxDepth := match rel with | none => none | some rel => some (synthPendingDepth + rel) }
933+
return (← applyCached abstResult?)
934+
if let some (depth, abstResult?) := entry.exact? then
935+
if depth == synthPendingDepth then
936+
-- The result's synthesis hit the `maxSynthPendingDepth` give-up, so reusing it
937+
-- keeps the enclosing query depth-exact as well.
938+
foldActivity { maxDepth := some synthPendingDepth, guardHit := true }
939+
return (← applyCached abstResult?)
940+
trace[Meta.synthInstance.cache] "new{depthSuffix synthPendingDepth}: {type}"
941+
let activityRef ← IO.mkRef {}
942+
let abstResult? ← withReader (fun ctx => { ctx with synthPendingActivityRef? := some activityRef }) do
943+
withNewMCtxDepth (allowLevelAssignments := true) do
944+
match kind with
945+
| .noMVars =>
946+
/-
947+
**Note**: The expensive `preprocessOutParam` step is morally **not** needed here because
948+
the output params should be uniquely determined by the input params. During type class
949+
resolution, definitional equality only unfolds `[reducible]` and `[instance_reducible]`
950+
declarations. This is a contract with our users to ensure performance is reasonable.
951+
However, the same `OrderDual` declaration that creates problems for `assignOutParams`
952+
also prevents us from using this optimization. As an example, suppose we are trying to
953+
synthesize
954+
```
955+
FunLike F (OrderDual α) (OrderDual β)
956+
```
957+
where the last two arguments of `FunLike` are output parameters. This term has no
958+
metavariables, and it seems natural to skip `preprocessOutParam`, which would replace
959+
the last two arguments with metavariables. However, if we don't replace them,
960+
TC resolution fails because it cannot unfold `OrderDual` since it is semireducible.
961+
962+
**Note**: We should remove `preprocessOutParam` from the following line as soon as
963+
Mathlib refactors `OrderDual`.
964+
-/
965+
SynthInstance.main (← preprocessOutParam type) maxResultSize
966+
| .mvarsNoOutputParams => SynthInstance.main type maxResultSize
967+
| .mvarsOutputParams => SynthInstance.main (← preprocessOutParam type) maxResultSize
968+
let result? ← applyAbstractResult? type abstResult?
969+
trace[Meta.synthInstance] "result {result?}"
970+
let activity ← activityRef.get
971+
foldActivity activity
972+
cacheResult key synthPendingDepth activity kind abstResult? result?
973+
return result?
966974

967975
def synthInstance? (type : Expr) (maxResultSize? : Option Nat := none) : MetaM (Option Expr) := do profileitM Exception "typeclass inference" (← getOptions) (decl := type.getAppFn.constName?.getD .anonymous) do
968976
synthInstanceCore? type maxResultSize?

0 commit comments

Comments
 (0)