Zulip Chat Archive

Stream: lean4

Topic: New compiler performance regression


Matej Penciak (Oct 20 2025 at 14:46):

It looks like the new compiler is hanging on a weird expression we're generating by elaborating a DSL we have. Here's a minimized example demonstrating the issue (it could probably be minimized further, but I'm running out of ideas how):

import Lean

open Lean

inductive Val (rep : Type) : Type where
| unit : Val rep
| array : (v : Vector rep n)  Val rep
| letIn : Val rep  (rep  Val rep)  Val rep
| call : (args : List rep)  Val rep

structure Function where
  body : List rep  Val rep

open Elab Command

abbrev DSLM := StateT Nat CommandElabM

def DSLM.run  (a : DSLM α) : CommandElabM α :=
  StateT.run' a 0

def nameOf : DSLM Lean.Ident :=
  modifyGet fun s =>
    (mkIdent (.mkSimple s!"#v_{s}"), s + 1)

def wrapInLet (e : TSyntax `term) (k : Option $ TSyntax `term  DSLM (TSyntax `term))
    : DSLM (TSyntax `term) := do
  let ident  nameOf
  match k with
  | some k => do
    let rest  k ident
    ``(Val.letIn $e fun $ident => $rest)
  | none => do
    pure e

def makeBlock (fuel : Nat) (k : Option $ TSyntax `term  DSLM (TSyntax `term))
  : DSLM (TSyntax `term) := do
  match fuel with
  | n + 1 => wrapInLet (←``(Val.unit)) $ some fun _ => makeBlock n k
  | 0 =>
    let numBinders  get
    let names : Array $ TSyntax `term :=
      Array.range numBinders |>.map (fun i => mkIdent (.mkSimple s!"#v_{i}"))
    let args  `([$names,*])
    wrapInLet (←``(Val.call $args)) k

def makeWeirdExpr (fuel : Nat) : DSLM (TSyntax `term) := do
  makeBlock fuel none

set_option trace.profiler true

elab "#foo" : command => do
  let body  DSLM.run $ makeWeirdExpr 150
  let lambda  `(fun args => match args with | _ => $body)
  let decl  `(def $(mkIdent `foo) : Function := Function.mk $lambda)
  elabCommand decl

#foo

#print foo

In 4.21 the command #foo was pretty much instant, but in toolchains >4.21 this particular command hangs for a significant amount of time, and if you increase the fuel in the elab definition further it will eventually fail to compile.

The output of the profiler seems to suggest the vast majority of the time spent in elaborating + compilation is in the dead code elimination pass, but I'm not sure if that's a red herring.

I'm happy to open an issue if this is something that should be looked at closer.

Henrik Böving (Oct 20 2025 at 16:16):

It's not stuck in the general dead code elimination pass but in some slightly smarter component that I can very much believe to be slow. Please do make an issue about it

Henrik Böving (Oct 20 2025 at 16:20):

https://share.firefox.dev/4qj6E4R slowness is in the widening of the analysis

Henrik Böving (Oct 20 2025 at 18:05):

The reason for the slowness is as follows: Your code generates a ton of nested lambdas with local variables which our analysis takes a look at. In particular this analysis uses a widening operator to speed up convergence for global function definition arguments but not for local function arguments etc. Crucially in the old compiler when elim dead branches ran we already did run lambda lifting so the widening operator speeds up convergence quite a lot. On the contrary in the new compiler we have not yet run lambda lifting and thus track the local function arguments in full precision.

To at least get similar behavior I'll try to teach the new compiler to just use widening for local function arguments as well.

Matej Penciak (Oct 20 2025 at 19:36):

Thanks for the explanation! I guess profiling of elimDeadBranches was indeed a bit of a red herring, but I'm glad it helped focus on the real issue.

I filed the issue at lean4#10857

Henrik Böving (Oct 20 2025 at 20:03):

No the issue is precisely in elimDeadBranches

Henrik Böving (Oct 20 2025 at 20:05):

It's just that elim dead branches is more than a syntactic dead code eliminator, that lives in a different component


Last updated: Dec 20 2025 at 21:32 UTC