perf: improve heuristic at isDefEq (#3837)

This is intended to fail at present: it just adds a test case containing
a minimization of a Mathlib slowdown from #3807.

Prior to #3807, the declaration `exists_algHom_adjoin_of_splits'''` at
the end of the file would take around 16,000 heartbeats. Now it takes
around 210,000 heartbeats.

---------

Co-authored-by: Leonardo de Moura <leomoura@amazon.com>
This commit is contained in:
Kim Morrison 2024-04-22 01:27:44 +02:00 committed by GitHub
parent 69202d9b73
commit ac0f699775
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2579 additions and 12 deletions

View file

@ -1173,6 +1173,35 @@ private def isDefEqLeftRight (fn : Name) (t s : Expr) : MetaM LBool := do
trace[Meta.isDefEq.delta.unfoldLeftRight] fn
toLBoolM <| Meta.isExprDefEqAux t s
/-- Helper predicate for `tryHeuristic`. -/
private def isNonTrivialRegular (info : DefinitionVal) : MetaM Bool := do
match info.hints with
| .regular d =>
if (← isProjectionFn info.name) then
-- All projections are considered trivial
return false
if d > 2 then
-- If definition depth is greater than 2, we claim it is not a trivial definition
return true
-- After consuming the lambda expressions, we consider a regular definition non-trivial if it is not "simple".
-- Where simple is a bvar/lit/sort/proj or a single application where all arguments are bvar/lit/sort/proj.
let val := consumeDefnPreamble info.value
return !isSimple val (allowApp := true)
| _ => return false
where
consumeDefnPreamble (e : Expr) : Expr :=
match e with
| .mdata _ e => consumeDefnPreamble e
| .lam _ _ b _ => consumeDefnPreamble b
| _ => e
isSimple (e : Expr) (allowApp : Bool) : Bool :=
match e with
| .bvar .. | .sort .. | .lit .. | .fvar .. | .mvar .. => true
| .app f a => isSimple a false && isSimple f allowApp
| .proj _ _ b => isSimple b false
| .mdata _ b => isSimple b allowApp
| .lam .. | .letE .. | .forallE .. | .const .. => false
/-- Try to solve `f a₁ ... aₙ =?= f b₁ ... bₙ` by solving `a₁ =?= b₁, ..., aₙ =?= bₙ`.
Auxiliary method for isDefEqDelta -/
@ -1181,19 +1210,32 @@ private def tryHeuristic (t s : Expr) : MetaM Bool := do
let mut s := s
let tFn := t.getAppFn
let sFn := s.getAppFn
let info ← getConstInfo tFn.constName!
/- We only use the heuristic when `f` is a regular definition or an auxiliary `match` application.
That is, it is not marked an abbreviation (e.g., a user-facing projection) or as opaque (e.g., proof).
We check whether terms contain metavariables to make sure we can solve constraints such
as `S.proj ?x =?= S.proj t` without performing delta-reduction.
That is, we are assuming the heuristic implemented by this method is seldom effective
when `t` and `s` do not have metavariables, are not structurally equal, and `f` is an abbreviation.
On the other hand, by unfolding `f`, we often produce smaller terms.
-- If `f` (i.e., `tFn`) is not a definition, we do not apply the heuristic.
let .defnInfo info ← getConstInfo tFn.constName! | return false
/-
We apply the heuristic in the following cases:
1- `f` is a non-trivial regular definition (see predicate `isNonTrivialRegular`)
2- `f` is `match` application.
3- `t` or `s` contain meta-variables.
Recall that auxiliary `match` definitions are marked as abbreviations, but we must use the heuristic on
them since they will not be unfolded when smartUnfolding is turned on. The abbreviation annotation in this
case is used to help the kernel type checker. -/
unless info.hints.isRegular || isMatcherCore (← getEnv) tFn.constName! do
The third case is important to make sure we can solve constraints such as
`S.proj ?x =?= S.proj t` without performing delta-reduction.
When the conditions 1&2&3 do not hold, we are assuming the heuristic implemented by this method is seldom effective
when `f` is not simple, `t` and `s` do not have metavariables, are not structurally equal.
Recall that auxiliary `match` definitions are marked as abbreviations, but we must use the heuristic on
them since they will not be unfolded when smartUnfolding is turned on. The abbreviation annotation in this
case is used to help the kernel type checker.
The `isNonTrivialRegular` predicate is also useful to avoid applying the heuristic to very simple definitions that
have not been marked as abbreviations by the user. Example:
```
protected def Mem (a : α) (s : Set α) : Prop := s a
```
at test 3807.lean
-/
unless (← isNonTrivialRegular info) || isMatcherCore (← getEnv) tFn.constName! do
unless t.hasExprMVar || s.hasExprMVar do
return false
withTraceNodeBefore `Meta.isDefEq.delta (return m!"{t} =?= {s}") do

2525
tests/lean/run/3807.lean Normal file

File diff suppressed because it is too large Load diff