Skip to content

Commit 47cd72d

Browse files
authored
perf: use WHNF cache in whnfMatcher also at sub-default transparency (#14323)
This PR addresses a performance regression from #13895 where `Lake.Config.ConfigDecl`, and more concretely the realization of `ConfigType.eq_1`, becomes significantly slower. The underlying issue uncovered by #13895 is that `whnfMatcher` uses a custom "can unfold?" predicate, which indirectly disables the WHNF cache because functions cannot be used as cache keys, when at reducible, instances or implicit transparency. This PR splits the "can unfold?" predicate in `Context` into two parts. `Context` still contains an optional custom predicate that, when used, disables the cache. But `whnfMatcher` does not use it anymore. `Config`, the cacheable structure, gets a new Boolean flag indicating whether we are at a matcher. Depending on this flag, one of two hard-coded predicates is used.
1 parent d3b9c57 commit 47cd72d

8 files changed

Lines changed: 281 additions & 65 deletions

File tree

src/Lean/Meta/Basic.lean

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ def ProjReductionKind.toUInt64 : ProjReductionKind → UInt64
8080
| .yesWithDelta => 2
8181
| .yesWithDeltaI => 3
8282

83+
structure CanUnfoldPredicateConfig where
84+
toBool : Bool
85+
deriving Inhabited, Repr
86+
87+
@[inline, expose, match_pattern]
88+
def CanUnfoldPredicateConfig.default : CanUnfoldPredicateConfig := ⟨false
89+
90+
@[inline, expose, match_pattern]
91+
def CanUnfoldPredicateConfig.atMatcher : CanUnfoldPredicateConfig := ⟨true
92+
8393
/--
8494
Configuration flags for the `MetaM` monad.
8595
Many of them are used to control the `isDefEq` function that checks whether two terms are definitionally equal or not.
@@ -194,6 +204,11 @@ structure Config where
194204
When `zeta := true`, then `zetaHave := false` disables zeta reduction of `have` expressions.
195205
-/
196206
zetaHave : Bool := true
207+
/--
208+
A predicate to control whether a constant can be unfolded or not at `whnf`.
209+
Ignored if `Context.customCanUnfoldPredicate?` is set.
210+
-/
211+
canUnfoldPredicateConfig : CanUnfoldPredicateConfig := .default
197212
deriving Inhabited, Repr
198213

199214
/-- Convert `isDefEq` and `WHNF` relevant parts into a key for caching results -/
@@ -216,7 +231,8 @@ private def Config.toKey (c : Config) : UInt64 :=
216231
(c.etaStruct.toUInt64 <<< 17) |||
217232
(c.proj.toUInt64 <<< 19) |||
218233
(c.zetaHave.toUInt64 <<< 21) |||
219-
(c.zetaUnused.toUInt64 <<< 22)
234+
(c.zetaUnused.toUInt64 <<< 22) |||
235+
(c.canUnfoldPredicateConfig.toBool.toUInt64 <<< 23)
220236

221237
/-- Configuration with key produced by `Config.toKey`. -/
222238
structure ConfigWithKey where
@@ -230,6 +246,16 @@ instance : Inhabited ConfigWithKey where -- #9463
230246
def Config.toConfigWithKey (c : Config) : ConfigWithKey :=
231247
{ config := c }
232248

249+
def ConfigWithKey.withCanUnfoldAtMatcherPred (c : ConfigWithKey) : ConfigWithKey :=
250+
{ config := { c.config with canUnfoldPredicateConfig := .atMatcher },
251+
key :=
252+
have : CanUnfoldPredicateConfig.atMatcher.toBool = true := rfl
253+
c.key ||| ((1 : UInt64) <<< 23) }
254+
255+
def ConfigWithKey.setTransparency (transparency : TransparencyMode) (c : ConfigWithKey) : ConfigWithKey :=
256+
{ config := { c.config with transparency }
257+
key := ((c.key >>> (3 : UInt64)) <<< 3) ||| transparency.toUInt64 }
258+
233259
/--
234260
Function parameter information cache.
235261
-/
@@ -494,25 +520,29 @@ structure Context where
494520
/-- Not `none` when inside of an `isDefEq` test. See `PostponedEntry`. -/
495521
defEqCtx? : Option DefEqContext := none
496522
/--
497-
Track the number of nested `synthPending` invocations. Nested invocations can happen
498-
when the type class resolution invokes `synthPending`.
523+
Track the number of nested `synthPending` invocations. Nested invocations can happen
524+
when the type class resolution invokes `synthPending`.
499525
500-
Remark: `synthPending` fails if `synthPendingDepth > maxSynthPendingDepth`.
526+
Remark: `synthPending` fails if `synthPendingDepth > maxSynthPendingDepth`.
501527
-/
502528
synthPendingDepth : Nat := 0
503529
/--
504-
A predicate to control whether a constant can be unfolded or not at `whnf`.
505-
Note that we do not cache results at `whnf` when `canUnfold?` is not `none`. -/
506-
canUnfold? : Option (Config → ConstantInfo → CoreM Bool) := none
530+
A predicate to control whether a constant can be unfolded or not at `whnf`.
531+
If set, overrides `Config.canUnfoldPredicateConfig`.
532+
Note that we do not cache results at `whnf` when `canUnfold?` is not `none`.
533+
This field lives outside `Config` because it is not cacheable and its type does not have a `Repr`
534+
instance.
535+
-/
536+
customCanUnfoldPredicate? : Option (Config → ConstantInfo → CoreM Bool) := none
507537
/--
508538
When `Config.univApprox := true`, this flag is set to `true` when there is no
509539
progress processing universe constraints.
510540
-/
511541
univApprox : Bool := false
512542
/--
513543
`inTypeClassResolution := true` when `isDefEq` is invoked at `tryResolve` in the type class
514-
resolution module. We don't use `isDefEqProjDelta` when performing TC resolution due to performance issues.
515-
This is not a great solution, but a proper solution would require a more sophisticated caching mechanism.
544+
resolution module. We don't use `isDefEqProjDelta` when performing TC resolution due to performance issues.
545+
This is not a great solution, but a proper solution would require a more sophisticated caching mechanism.
516546
-/
517547
inTypeClassResolution : Bool := false
518548
/--
@@ -1172,7 +1202,12 @@ def elimMVarDeps (xs : Array Expr) (e : Expr) (preserveOrder : Bool := false) :
11721202
{ ctx with keyedConfig := { config } }
11731203

11741204
@[inline] def withCanUnfoldPred (p : Config → ConstantInfo → CoreM Bool) : n α → n α :=
1175-
mapMetaM <| withReader (fun ctx => { ctx with canUnfold? := p })
1205+
mapMetaM <| withReader (fun ctx => { ctx with customCanUnfoldPredicate? := some p })
1206+
1207+
@[inline] def withCanUnfoldAtMatcherPred : n α → n α :=
1208+
mapMetaM <| withReader (fun ctx => { ctx with
1209+
customCanUnfoldPredicate? := none,
1210+
keyedConfig := ctx.keyedConfig.withCanUnfoldAtMatcherPred })
11761211

11771212
@[inline] def withIncSynthPending : n α → n α :=
11781213
mapMetaM <| withReader (fun ctx => { ctx with synthPendingDepth := ctx.synthPendingDepth + 1 })
@@ -1264,11 +1299,9 @@ def withTrackingZetaDeltaSet (s : FVarIdSet) : n α → n α :=
12641299
@[inline] def withoutProofIrrelevance (x : n α) : n α :=
12651300
withConfig (fun cfg => { cfg with proofIrrelevance := false }) x
12661301

1267-
@[inline] private def Context.setTransparency (ctx : Context) (transparency : TransparencyMode) : Context :=
1268-
let config := { ctx.config with transparency }
1269-
-- Recall that `transparency` is stored in the first 3 bits (it has 5 values).
1270-
let key : UInt64 := ((ctx.configKey >>> (3 : UInt64)) <<< 3) ||| transparency.toUInt64
1271-
{ ctx with keyedConfig := { config, key } }
1302+
@[inline]
1303+
private def Context.setTransparency (ctx : Context) (transparency : TransparencyMode) : Context :=
1304+
{ ctx with keyedConfig := ctx.keyedConfig.setTransparency transparency }
12721305

12731306
@[inline] def withTransparency (mode : TransparencyMode) : n α → n α :=
12741307
-- We avoid `withConfig` for performance reasons.

src/Lean/Meta/ExprDefEq.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2236,7 +2236,7 @@ inductive DefEqCacheKind where
22362236
| permanent -- problem does not have mvars and we are using standard config, we can use one persistent cache.
22372237

22382238
private def getDefEqCacheKind (t s : Expr) : MetaM DefEqCacheKind := do
2239-
if t.hasMVar || s.hasMVar || (← read).canUnfold?.isSome then
2239+
if t.hasMVar || s.hasMVar || (← read).customCanUnfoldPredicate?.isSome then
22402240
return .transient
22412241
else
22422242
return .permanent

src/Lean/Meta/GetUnfoldableConst.lean

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Authors: Leonardo de Moura
66
module
77
prelude
88
public import Lean.Meta.Basic
9+
public import Lean.Meta.Match.MatchPatternAttr
910
public section
1011
namespace Lean.Meta
1112

@@ -29,13 +30,42 @@ def canUnfoldDefault (cfg : Config) (info : ConstantInfo) : CoreM Bool := do
2930
else
3031
return false
3132

33+
/--
34+
Alternative can-unfold predicate used inside `whnfMatcher`.
35+
See module comment above `unfoldNestedDIte` in `Lean.Meta.WHNF`.
36+
-/
37+
def canUnfoldAtMatcher (cfg : Config) (info : ConstantInfo) : CoreM Bool := do
38+
if (← canUnfoldDefault cfg info) then
39+
return true
40+
/- Beyond what the normal transparency allows, we additionally unfold
41+
certain definitions to expose constructors in match discriminants. -/
42+
if hasMatchPatternAttribute (← getEnv) info.name then
43+
return true
44+
return info.name == ``OfNat.ofNat -- needed to reduce numeric literals in match discriminants
45+
|| info.name == ``NatCast.natCast -- needed for `↑m` in match discriminants (pervasive in Int proofs)
46+
|| info.name == ``Zero.zero || info.name == ``One.one -- needed for `0`/`1` in match discriminants
47+
|| info.name == ``decEq
48+
|| info.name == ``Nat.decEq
49+
|| info.name == ``Char.ofNat || info.name == ``Char.ofNatAux
50+
|| info.name == ``String.decEq || info.name == ``List.hasDecEq
51+
|| info.name == ``Fin.ofNat -- needed for Fin literal reduction in match discriminants
52+
|| info.name == ``UInt8.ofNat || info.name == ``UInt8.decEq
53+
|| info.name == ``UInt16.ofNat || info.name == ``UInt16.decEq
54+
|| info.name == ``UInt32.ofNat || info.name == ``UInt32.decEq
55+
|| info.name == ``UInt64.ofNat || info.name == ``UInt64.decEq
56+
/- `Fin.ofNat` reduces to `⟨a % n, _⟩`, so we also need to unfold `%` (i.e., `HMod.hMod`
57+
and `Mod.mod`) to expose the `Fin.mk` constructor in match discriminants. -/
58+
|| info.name == ``HMod.hMod || info.name == ``Mod.mod
59+
3260
def canUnfold (info : ConstantInfo) : MetaM Bool := do
3361
let ctx ← read
3462
let cfg ← getConfig
35-
if let some f := ctx.canUnfold? then
36-
f cfg info
37-
else
38-
canUnfoldDefault cfg info
63+
match ctx.customCanUnfoldPredicate? with
64+
| some f => f cfg info
65+
| none =>
66+
match ctx.config.canUnfoldPredicateConfig with
67+
| .default => canUnfoldDefault cfg info
68+
| .atMatcher => canUnfoldAtMatcher cfg info
3969

4070
/--
4171
Look up a constant name, returning the `ConstantInfo`

src/Lean/Meta/Tactic/Cbv/ControlFlow.lean

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -290,23 +290,16 @@ This prevents kernel-level reduction (used by `reduceRecMatcher?` and `reducePro
290290
from bypassing the `@[cbv_opaque]` attribute.
291291
-/
292292
public def withCbvOpaqueGuard (x : MetaM α) : MetaM α := do
293-
let prev := (← readThe Meta.Context).canUnfold?
293+
let prevCustomCanUnfoldPredicate? := (← readThe Meta.Context).customCanUnfoldPredicate?
294+
let prevCanUnfoldPredicateConfig := (← readThe Meta.Context).config.canUnfoldPredicateConfig
294295
withCanUnfoldPred (fun cfg info => do
295296
if (← isCbvOpaque info.name) then return false
296-
match prev with
297-
| some f => f cfg info
298-
| none =>
299-
-- Duplicates `canUnfoldDefault` from `Lean.Meta.GetUnfoldableConst` (private).
300-
match cfg.transparency with
301-
| .none => return false
302-
| .all => return true
303-
| .default => return !(← isIrreducible info.name)
304-
| m =>
305-
let status ← getReducibilityStatus info.name
306-
if status == .reducible then return true
307-
else if status == .instanceReducible && (m == .instances || m == .implicit) then return true
308-
else if status == .implicitReducible && m == .implicit then return true
309-
else return false
297+
match prevCustomCanUnfoldPredicate? with
298+
| .some f => f cfg info
299+
| .none =>
300+
match prevCanUnfoldPredicateConfig with
301+
| .default => canUnfoldDefault cfg info
302+
| .atMatcher => canUnfoldAtMatcher cfg info
310303
) x
311304

312305
builtin_cbv_simproc ↓ simpCbvCond (@cond _ _ _) := simpCond

src/Lean/Meta/Tactic/Simp/Rewrite.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ private def dischargeUsingAssumption? (e : Expr) : SimpM (Option Expr) := do
595595
partial def dischargeEqnThmHypothesis? (e : Expr) : MetaM (Option Expr) := do
596596
assert! isEqnThmHypothesis e
597597
let mvar ← mkFreshExprSyntheticOpaqueMVar e
598-
withCanUnfoldPred canUnfoldAtMatcher do
598+
withCanUnfoldAtMatcherPred do
599599
if let .none ← go? mvar.mvarId! then
600600
instantiateMVars mvar
601601
else

src/Lean/Meta/WHNF.lean

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -491,33 +491,6 @@ private def unfoldNestedDIte (e : Expr) : CoreM Expr := do
491491
else
492492
return e
493493

494-
/--
495-
Auxiliary predicate for `whnfMatcher`.
496-
See comment above.
497-
-/
498-
def canUnfoldAtMatcher (cfg : Config) (info : ConstantInfo) : CoreM Bool := do
499-
if (← canUnfoldDefault cfg info) then
500-
return true
501-
/- Beyond what the normal transparency allows, we additionally unfold
502-
certain definitions to expose constructors in match discriminants. -/
503-
if hasMatchPatternAttribute (← getEnv) info.name then
504-
return true
505-
return info.name == ``OfNat.ofNat -- needed to reduce numeric literals in match discriminants
506-
|| info.name == ``NatCast.natCast -- needed for `↑m` in match discriminants (pervasive in Int proofs)
507-
|| info.name == ``Zero.zero || info.name == ``One.one -- needed for `0`/`1` in match discriminants
508-
|| info.name == ``decEq
509-
|| info.name == ``Nat.decEq
510-
|| info.name == ``Char.ofNat || info.name == ``Char.ofNatAux
511-
|| info.name == ``String.decEq || info.name == ``List.hasDecEq
512-
|| info.name == ``Fin.ofNat -- needed for Fin literal reduction in match discriminants
513-
|| info.name == ``UInt8.ofNat || info.name == ``UInt8.decEq
514-
|| info.name == ``UInt16.ofNat || info.name == ``UInt16.decEq
515-
|| info.name == ``UInt32.ofNat || info.name == ``UInt32.decEq
516-
|| info.name == ``UInt64.ofNat || info.name == ``UInt64.decEq
517-
/- `Fin.ofNat` reduces to `⟨a % n, _⟩`, so we also need to unfold `%` (i.e., `HMod.hMod`
518-
and `Mod.mod`) to expose the `Fin.mk` constructor in match discriminants. -/
519-
|| info.name == ``HMod.hMod || info.name == ``Mod.mod
520-
521494
private def whnfMatcher (e : Expr) : MetaM Expr := do
522495
/- When reducing `match` expressions at `.reducible`, `.instances` or `.implicit` transparency,
523496
we use a custom `canUnfoldAtMatcher` predicate that additionally allows unfolding
@@ -526,7 +499,7 @@ private def whnfMatcher (e : Expr) : MetaM Expr := do
526499
reduced to expose constructors, without bumping the overall transparency level. -/
527500
if (← getTransparency) matches .reducible | .instances | .implicit then
528501
-- Also unfold some default-reducible constants; see `canUnfoldAtMatcher`
529-
withCanUnfoldPred canUnfoldAtMatcher do
502+
withCanUnfoldAtMatcherPred do
530503
whnf e
531504
else
532505
-- Do NOT use `canUnfoldAtMatcher` here as it does not affect all/default reducibility and inhibits caching (#2564).
@@ -1082,7 +1055,7 @@ def reduceNat? (e : Expr) : MetaM (Option Expr) :=
10821055
@[inline] private def useWHNFCache (e : Expr) : MetaM Bool := do
10831056
-- We cache only closed terms without expr metavars.
10841057
-- Potential refinement: cache if `e` is not stuck at a metavariable
1085-
if e.hasFVar || e.hasExprMVar || (← read).canUnfold?.isSome then
1058+
if e.hasFVar || e.hasExprMVar || (← read).customCanUnfoldPredicate?.isSome then
10861059
return false
10871060
else
10881061
return true

tests/elab/keyedConfig.lean

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module
2+
3+
import all Lean.Meta.Basic
4+
5+
/-!
6+
This file proves that optimized bit-fiddling operations on WHNF config keys are correct.
7+
-/
8+
9+
open Lean Meta
10+
11+
/-!
12+
# Preparations
13+
-/
14+
15+
/--
16+
A term shifted left by `k` (with `3 ≤ k < 64`) is unchanged by `>>> 3 <<< 3`
17+
(its bits all sit at position ≥ 3, so clearing the low 3 bits is a no-op).
18+
-/
19+
theorem shiftLeft_shiftRight_shiftLeft_of_three_le (x k : UInt64) (hk3 : 3 ≤ k.toNat) (hk64 : k.toNat < 64) :
20+
(x <<< k) >>> (3:UInt64) <<< 3 = x <<< k := by
21+
apply UInt64.toBitVec_inj.1
22+
rw [BitVec.eq_of_getLsbD_eq_iff]
23+
intro i hi
24+
have hkn : (k.toBitVec % 64).toNat = k.toNat := by
25+
rw [BitVec.toNat_umod]; exact Nat.mod_eq_of_lt hk64
26+
simp only [UInt64.toBitVec_shiftLeft, UInt64.toBitVec_shiftRight,
27+
BitVec.shiftLeft_eq', BitVec.ushiftRight_eq',
28+
BitVec.getLsbD_shiftLeft, BitVec.getLsbD_ushiftRight,
29+
show (UInt64.toBitVec 3 % 64).toNat = 3 from by decide, hkn]
30+
rw [show 3 + (i - 3) - k.toNat = i - k.toNat from by omega]
31+
rcases Nat.lt_or_ge i 3 with h|h <;> rcases Nat.lt_or_ge i k.toNat with h'|h' <;>
32+
simp_all <;> omega
33+
34+
/-!
35+
# Proofs
36+
-/
37+
38+
example (c : ConfigWithKey) (h : c.config.toKey = c.key) :
39+
let c' := c.withCanUnfoldAtMatcherPred
40+
c'.config.toKey = c'.key := by
41+
obtain ⟨cfg, k⟩ := c
42+
simp only [ConfigWithKey.withCanUnfoldAtMatcherPred] at h ⊢
43+
subst h
44+
simp only [Config.toKey, CanUnfoldPredicateConfig.atMatcher]
45+
cases cfg.canUnfoldPredicateConfig.toBool <;>
46+
simp [Bool.toUInt64, UInt64.or_assoc, UInt64.or_self, UInt64.or_zero]
47+
48+
example (c : ConfigWithKey) (t : TransparencyMode) (h : c.config.toKey = c.key) :
49+
let c' := c.setTransparency t
50+
c'.config.toKey = c'.key := by
51+
obtain ⟨cfg, k⟩ := c
52+
simp only [ConfigWithKey.setTransparency] at h ⊢
53+
subst h
54+
simp only [Config.toKey, UInt64.shiftRight_or, UInt64.shiftLeft_or,
55+
show cfg.transparency.toUInt64 >>> (3:UInt64) = 0 from by cases cfg.transparency <;> rfl,
56+
UInt64.zero_or]
57+
repeat rw [shiftLeft_shiftRight_shiftLeft_of_three_le _ _ (by decide) (by decide)]
58+
-- Without generalizing these away, the proof is too slow.
59+
-- We could use `bv_decide` at the cost of heavier imports.
60+
generalize (_ : UInt64) <<< _ = a,
61+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
62+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
63+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
64+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
65+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
66+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
67+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
68+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a,
69+
(_ : UInt64) <<< _ = a, (_ : UInt64) <<< _ = a
70+
simp only [UInt64.or_assoc, UInt64.or_comm]

0 commit comments

Comments
 (0)