Skip to content

Commit de7e9f4

Browse files
committed
perf: merge the imported extra-constants view lazily
The extra-constants view (codegen names consulted only when a name misses the constants view) was merged eagerly at import like the constants view, although its duplicate report is not consumed. Merge it lazily instead: a lazy node holds the candidate subtrees sharing its prefix and computes its children on first descent via a thunk, one level at a time; entry lookups at the prefix itself answer directly from the candidates. Thunk forcing is thread-safe and supported on persistent objects, so lazy nodes are safe to share across elaboration threads, and the process-global lookup cache keeps each forced walk to once per name. This removes the extras merge (over half the total merge work) from the import path: `import Mathlib` drops 7% task-clock locally, and even extras-heavy elaboration workloads come out slightly ahead since only touched prefixes are ever merged. Also ports the leanchecker `ReplaceAxiom` helper to the `modifyAt`/`mkMerged` API, which the new constructor's exhaustiveness requirement would otherwise break. Co-Authored-By: Claude
1 parent e5ae1c9 commit de7e9f4

3 files changed

Lines changed: 111 additions & 24 deletions

File tree

src/Lean/ConstTrie.lean

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ public def ofNames (names : Array Name) : ConstTrie Unit := Id.run do
209209

210210
end ConstTrie
211211

212+
-- `Thunk` fields have no `sizeOf` theorems
213+
set_option genSizeOfSpec false in
212214
/--
213215
Merged view of the per-module constant prefix trees of all imported modules. Node keys and lookup
214216
work like in `ConstTrie`; subtrees whose prefix occurs in only a single module are borrowed
@@ -223,6 +225,15 @@ public inductive ImportedConsts (α : Type) where
223225
-/
224226
| merged (key : Name) (entry? : Option (α × Nat)) (children : Array (ImportedConsts α))
225227
(childIndex : ByteArray)
228+
/--
229+
Like `merged`, but with the children computed on first descent from the candidate subtrees
230+
sharing the prefix, one level at a time. Thunk forcing is thread-safe and supported on
231+
persistent objects, so lazy nodes are safe to share across elaboration threads. Used for views
232+
whose `Duplicate`s nobody consumes; entry lookups at the prefix itself answer directly from
233+
`cands` without forcing.
234+
-/
235+
| lazy (key : Name) (cands : Array (Nat × ConstTrie α))
236+
(expanded : Thunk (Array (ImportedConsts α) × ByteArray))
226237

227238
public instance : Inhabited (ImportedConsts α) := ⟨.merged .anonymous none #[] .empty⟩
228239

@@ -233,15 +244,31 @@ public def empty : ImportedConsts α := .merged .anonymous none #[] .empty
233244
def key : ImportedConsts α → Name
234245
| .mod _ t => t.key
235246
| .merged k .. => k
247+
| .lazy k .. => k
236248

237249
/-- Creates a `merged` node, computing the child index; `children` must be sorted by key hash. -/
238250
public def mkMerged (key : Name) (entry? : Option (α × Nat))
239251
(children : Array (ImportedConsts α)) : ImportedConsts α :=
240252
.merged key entry? children (buildChildIndex children ImportedConsts.key)
241253

254+
/-- The entry of the prefix shared by `cands`: the first candidate value, with its module. -/
255+
private def candsEntry? (cands : Array (Nat × ConstTrie α)) : Option (α × Nat) := Id.run do
256+
for (i, t) in cands do
257+
if let some v := t.val? then
258+
return some (v, i)
259+
return none
260+
242261
/-- Walks from `t`, the node for `path[i]` (or the root for `i = path.size`), to `path[0]`. -/
243262
private partial def findValAux : ImportedConsts α → Array Name → Nat → Option α
244263
| .mod _ tr, path, i => tr.findAux path i
264+
| .lazy _ cands expanded, path, i =>
265+
if i == 0 then
266+
(candsEntry? cands).map (·.1)
267+
else
268+
let (cs, hs) := expanded.get
269+
match findHashIdx? hs cs key path[i - 1]! with
270+
| some j => findValAux cs[j]! path (i - 1)
271+
| none => none
245272
| .merged _ e? cs hs, path, i =>
246273
if i == 0 then
247274
match e? with
@@ -257,6 +284,14 @@ private partial def findModIdxAux : ImportedConsts α → Array Name → Nat →
257284
match tr.findAux path i with
258285
| some _ => some modIdx
259286
| none => none
287+
| .lazy _ cands expanded, path, i =>
288+
if i == 0 then
289+
(candsEntry? cands).map (·.2)
290+
else
291+
let (cs, hs) := expanded.get
292+
match findHashIdx? hs cs key path[i - 1]! with
293+
| some j => findModIdxAux cs[j]! path (i - 1)
294+
| none => none
260295
| .merged _ e? cs hs, path, i =>
261296
if i == 0 then
262297
match e? with
@@ -272,6 +307,14 @@ private partial def findEntryAux : ImportedConsts α → Array Name → Nat →
272307
match tr.findAux path i with
273308
| some v => some (v, modIdx)
274309
| none => none
310+
| .lazy _ cands expanded, path, i =>
311+
if i == 0 then
312+
candsEntry? cands
313+
else
314+
let (cs, hs) := expanded.get
315+
match findHashIdx? hs cs key path[i - 1]! with
316+
| some j => findEntryAux cs[j]! path (i - 1)
317+
| none => none
275318
| .merged _ e? cs hs, path, i =>
276319
if i == 0 then
277320
e?
@@ -392,6 +435,55 @@ public def mergeModuleTrees (trees : Array (Nat × ConstTrie α)) :
392435
ImportedConsts α × Array (Duplicate α) :=
393436
mergeCands .anonymous trees |>.run #[]
394437

438+
/-- `mergeCands` without duplicate reporting, producing lazily expanded nodes. -/
439+
private partial def mergeLazy (key : Name) (cands : Array (Nat × ConstTrie α)) :
440+
ImportedConsts α :=
441+
if cands.size == 1 then
442+
let (i, t) := cands[0]!
443+
.mod i t
444+
else
445+
.lazy key cands (Thunk.mk fun _ => expandLazy cands)
446+
where
447+
expandLazy (cands : Array (Nat × ConstTrie α)) :
448+
Array (ImportedConsts α) × ByteArray := Id.run do
449+
let mut total := 0
450+
for (_, t) in cands do
451+
total := total + t.children.size
452+
let mut all : Array (UInt64 × Nat × ConstTrie α) := .mkEmpty total
453+
for (i, t) in cands do
454+
for c in t.children do
455+
all := all.push (c.key.hash, i, c)
456+
let sorted := all.qsort fun (h₁, i₁, _) (h₂, i₂, _) => h₁ < h₂ || (h₁ == h₂ && i₁ < i₂)
457+
let mut children := #[]
458+
let mut idx := 0
459+
while idx < sorted.size do
460+
let (hash₀, i, c) := sorted[idx]!
461+
idx := idx + 1
462+
if idx == sorted.size || sorted[idx]!.1 != hash₀ then
463+
-- sole child with this key hash, so from a single module: borrow the subtree
464+
children := children.push (.mod i c)
465+
else
466+
-- gather the run of children with the same key hash, partitioned by actual key in case
467+
-- of hash collisions
468+
let mut groups : Array (Array (Nat × ConstTrie α)) := #[#[(i, c)]]
469+
while idx < sorted.size && sorted[idx]!.1 == hash₀ do
470+
let (_, i, c) := sorted[idx]!
471+
match groups.findIdx? fun g => lastPartEq g[0]!.2.key c.key with
472+
| some gi => groups := groups.modify gi (·.push (i, c))
473+
| none => groups := groups.push #[(i, c)]
474+
idx := idx + 1
475+
for g in groups do
476+
children := children.push (mergeLazy g[0]!.2.key g)
477+
return (children, buildChildIndex children ImportedConsts.key)
478+
479+
/--
480+
Like `mergeModuleTrees`, but merging each node's children on first descent instead of eagerly.
481+
No `Duplicate`s are reported; an entry declared by several modules resolves to the first
482+
module's value, as in the eager merge. Intended for views whose duplicates are not consumed.
483+
-/
484+
public def mergeModuleTreesLazy (trees : Array (Nat × ConstTrie α)) : ImportedConsts α :=
485+
mergeLazy .anonymous trees
486+
395487
/--
396488
Replaces the node for `n` using `f`. `n` must be a prefix shared by several modules, such as a
397489
`Duplicate` name; single-module subtrees are never modified.
@@ -406,6 +498,7 @@ where
406498
@[inline] modifyChild (n : Name) (f : ImportedConsts α → ImportedConsts α) :
407499
ImportedConsts α → ImportedConsts α
408500
| t@(.mod ..) => t
501+
| t@(.lazy ..) => t
409502
| .merged k e? cs hs =>
410503
match findHashIdx? hs cs key n with
411504
| some i => .merged k e? (cs.modify i f) hs
@@ -426,6 +519,11 @@ public partial def foldlM [Monad m] (t : ImportedConsts α) (f : σ → Name →
426519
if let some (v, _) := e? then
427520
s ← f s k v
428521
cs.foldlM (fun s c => c.foldlM f s) s
522+
| .lazy k cands expanded =>
523+
let mut s := init
524+
if let some (v, _) := candsEntry? cands then
525+
s ← f s k v
526+
(expanded.get).1.foldlM (fun s c => c.foldlM f s) s
429527

430528
public def foldl (t : ImportedConsts α) (f : σ → Name → α → σ) (init : σ) : σ :=
431529
t.foldlM (m := Id) f init
@@ -440,6 +538,11 @@ public partial def foldlEntriesM [Monad m] (t : ImportedConsts α)
440538
if let some e := e? then
441539
s ← f s k e
442540
cs.foldlM (fun s c => c.foldlEntriesM f s) s
541+
| .lazy k cands expanded =>
542+
let mut s := init
543+
if let some e := candsEntry? cands then
544+
s ← f s k e
545+
(expanded.get).1.foldlM (fun s c => c.foldlEntriesM f s) s
443546

444547
public def forM [Monad m] (t : ImportedConsts α) (f : Name → α → m PUnit) : m PUnit :=
445548
t.foldlM (fun _ n v => f n v) ⟨⟩

src/Lean/Environment.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,8 +2397,8 @@ def finalizeImport (s : ImportState) (imports : Array Import) (opts : Options) (
23972397
let (privImported, privDups) :=
23982398
ImportedConsts.mergeModuleTrees <| moduleData.mapIdx fun modIdx data => (modIdx, data.constTrie)
23992399
let privImported ← resolveDuplicates s privImported privDups (throwOnConflict := true)
2400-
let (importedExtraConsts, _) :=
2401-
ImportedConsts.mergeModuleTrees <| irData.mapIdx fun modIdx data => (modIdx, data.extraConstTrie)
2400+
let importedExtraConsts :=
2401+
ImportedConsts.mergeModuleTreesLazy <| irData.mapIdx fun modIdx data => (modIdx, data.extraConstTrie)
24022402
let mut publicTrees := #[]
24032403
if isModule then
24042404
for h : modIdx in *...modules.size do

tests/pkg/leanchecker/LeanCheckerTests/ReplaceAxiom.lean

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,13 @@ open private Lean.Kernel.Environment.mk from Lean.Environment
55
open private Lean.Kernel.Environment.extensions from Lean.Environment
66

77
open Lean in
8-
/-- Overwrites the entry for `n` in the imported constants view, for test purposes. -/
9-
partial def overrideImported (t : ImportedConsts ConstantInfo) (n : Name) (c : ConstantInfo) :
8+
/--
9+
Overwrites the entry for the childless name `n` in the imported constants view, for test purposes:
10+
`n`'s node is replaced by a merged leaf holding the forged value.
11+
-/
12+
def overrideImported (t : ImportedConsts ConstantInfo) (n : Name) (c : ConstantInfo) :
1013
ImportedConsts ConstantInfo :=
11-
modAt t n setter
12-
where
13-
setter : ImportedConsts ConstantInfo → ImportedConsts ConstantInfo
14-
| .merged k _ cs hs => .merged k (some (c, 0)) cs hs
15-
| .mod i (.node k _ cs hs) => .merged k (some (c, 0)) (cs.map (.mod i)) hs
16-
modAt (t : ImportedConsts ConstantInfo) (n : Name)
17-
(f : ImportedConsts ConstantInfo → ImportedConsts ConstantInfo) :
18-
ImportedConsts ConstantInfo :=
19-
match n with
20-
| .anonymous => f t
21-
| n => modAt t n.getPrefix (step n f)
22-
step (n : Name) (f : ImportedConsts ConstantInfo → ImportedConsts ConstantInfo) :
23-
ImportedConsts ConstantInfo → ImportedConsts ConstantInfo
24-
| .merged k e cs hs => (.merged k e · hs) <| cs.map fun c =>
25-
if keyOf c == n then f c else c
26-
| .mod i (.node k v cs hs) => (.merged k (v.map ((·, i))) · hs) <| cs.map fun c =>
27-
if (match c with | .node ck .. => ck) == n then f (.mod i c) else .mod i c
28-
keyOf : ImportedConsts ConstantInfo → Name
29-
| .mod _ (.node k ..) => k
30-
| .merged k .. => k
14+
t.modifyAt n fun _ => .mkMerged n (some (c, 0)) #[]
3115

3216
/- Redefine `propext : False`. -/
3317
open Lean Elab Meta in

0 commit comments

Comments
 (0)