leanprover / leanprover/lean4

LCNF simplifier scales super-linearly

Open
#13,632 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug code-generator P-medium
Dominant language
Lean
Stars
9.2k
Forks
990
Avg merge
1d 17h
Merged PRs (30d)
175

Description

Prerequisites
Description

The following Claude-produced MWE exhibits superlinear scaling:

import Lean

/-!
MWE: super-linear LCNF compile time on a sequential monadic do-block.

For each size N, `body_N` is `do <N copies of `if let some r ← step then return r`>;
return 0`. The elaborated term grows linearly in N, but `compilation (LCNF base)` is
super-linear — at N=64 it takes ~3.5 s.

The cost is essentially all in `pass simp` (`src/Lean/Compiler/LCNF/Simp.lean`).
`Decl.simp` is a fixpoint loop; iteration 1 dominates, and within it the
`# visited` counter (recursive `simp` calls) grows super-linearly:

  N=4 → 8 → 16 → 32 :  visited/size ratio = 10 → 12 → 15 → 22

`# inline local` (local function / join-point inlines) is exactly linear (one per
do-statement), so what's super-linear is *visits per inline*. Hypothesized
mechanism: monadic `if let some` desugars to a chain of N nested `__do_jp` join
points. Each JP inline copies the JP body into its call site, and the parent
`simp` re-traverses the copy. With N nested JPs of size O(N), total work is O(N²).

The choice of monad matters for the *severity*: deeper transformer stacks
(here `ReaderT Nat (StateRefT Nat MetaM)`) compound the inlining cost. Plain
`IO` shows the same shape but at smaller absolute numbers.

Measurements (stage1 lean 4.31.0-pre, commit 7df0bef7):

  N    LCNF base    pass simp    ratio vs N/2
  4    6.6 ms       ~6 ms        —
  8    21 ms        ~20 ms       3.2×
  16   55 ms        ~54 ms       2.6×
  32   270 ms       ~265 ms      4.9×
  64   3.52 s       ~3.5 s       13×

Run from the worktree root:

  build/release/stage1/bin/lean --root=. lcnf_simp_scaling_mwe.lean \
    2>&1 | grep -E '^(=====|compilation \(LCNF base\))'

To re-do per-pass attribution, instrument `runPassManagerPart` in
`src/Lean/Compiler/LCNF/Main.lean` by wrapping `pass.run` in
`profileitM Exception s!"pass {pass.name}" (← getOptions) ...`.
-/

open Lean Elab

abbrev MyM := ReaderT Nat (StateRefT Nat Lean.MetaM)

@[noinline] opaque step : MyM (Option Nat)

elab "mkBody " n:num : term => do
  let n := n.getNat
  let mut stmts : Array (TSyntax `Lean.Parser.Term.doSeqItem) := #[]
  for _ in [:n] do
    stmts := stmts.push (← `(Lean.Parser.Term.doSeqItem| if let some r ← step then return r))
  stmts := stmts.push (← `(Lean.Parser.Term.doSeqItem| return 0))
  let stx ← `(do $[$stmts]*)
  Term.elabTerm stx none

set_option profiler true
set_option profiler.threshold 100

set_option backward.do.legacy false -- to test with the new do elaborator (old reproduces the same)

#eval IO.eprintln "===== body_2 ====="
set_option trace.Elab.do true in
def body_2 : MyM Nat := mkBody 2
#eval IO.eprintln "===== body_30 ====="
def body_30 : MyM Nat := mkBody 30
#eval IO.eprintln "===== body_60 ====="
def body_60 : MyM Nat := mkBody 60
#eval IO.eprintln "===== body_80 ====="
def body_90 : MyM Nat := mkBody 90
Steps to Reproduce

Elaborate the file.

Expected behavior: Elaboration time for mkBody n scales linearly in n

Actual behavior: Elaboration time for mkBody n scales super-linearly in n

Versions

Lean 4.31.0, commit
Target: x86_64-unknown-linux-gnu

Impact

Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the provided lcnf_simp_scaling_mwe.lean command from the worktree root and inspect pass simp in src/Lean/Compiler/LCNF/Simp.lean. Use the suggested runPassManagerPart instrumentation in src/Lean/Compiler/LCNF/Main.lean to confirm the attribution; done means the reproducer's elaboration and simp work no longer scale super-linearly, with a regression test if the project provides an appropriate location.

Written by the indexing model from the issue text.

Assessment

Domain
compilers, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.