Skip to content

Commit 4c74135

Browse files
authored
faster shoggoth (#2525)
1 parent d1ae939 commit 4c74135

8 files changed

Lines changed: 218 additions & 89 deletions

File tree

src/hexer/intramodinliner.nim

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@
4343
## - `intraModuleInline` runs inside hexer, pre-DCE, on the tree that becomes
4444
## this module's `.x.nif`. Its own module only (`xnifDir` unset).
4545
## - `runInterModuleInliner` (shoggoth) runs after DCE, on the `.c.nif`s, and
46-
## is the one that crosses module borders: `loadForeign` lazy-loads the
47-
## callee's `.c.nif`.
46+
## is the one that crosses module borders: `foreignProc` parses just the
47+
## callee's decl out of its `.c.nif`, via the file's embedded index.
4848
##
4949
## Either way the decision is derived from the same file the body comes out
50-
## of: `indexProcBodies` measures each proc right after the module is parsed,
51-
## so what is scored is exactly what would be spliced (hexer's flattening of
50+
## of: `computeInlineInfo` measures the very decl a splice would copy (for the
51+
## own module in `indexProcBodies`, for a callee in `lookupInlineInfo`), so
52+
## what is scored is exactly what would be spliced (hexer's flattening of
5253
## tiny bodies happens *before* the `.c.nif` is written — a proc that grows
5354
## past the bound by having its own callees spliced into it is re-measured,
5455
## and demoted, by every importer).
@@ -58,11 +59,12 @@
5859
## the inlined body to fresh symbols, and drops the trailing `(ret X)`
5960
## (its result is discarded at statement position).
6061

61-
import std / [tables, assertions, os, sets, hashes]
62-
when defined(inlinerTrace) or defined(inlinerDeny): import std / [syncio, strutils]
62+
import std / [tables, assertions, os, sets, hashes, syncio]
63+
when defined(inlinerTrace) or defined(inlinerDeny): import std / [strutils]
6364
include ".." / lib / nifprelude
6465
include ".." / lib / compat2
6566
import ".." / lib / symparser
67+
import ".." / lib / foreignmodules
6668
import ".." / lengc / [leng_model]
6769

6870
type
@@ -268,11 +270,6 @@ proc computeInlineInfo*(procDecl: Cursor): InlineInfo =
268270
result.inlinable = true
269271

270272
type
271-
ForeignModule* = object
272-
buf*: TokenBuf
273-
bodies*: Table[SymId, int] # sym → offset of its (proc …) in `buf`
274-
inlineInfo*: Table[SymId, InlineInfo] # sym → its `(inline …)` annotation
275-
276273
InlinerCtx* = object
277274
moduleSuffix*: string
278275
counter: int # fresh-name suffix
@@ -304,9 +301,14 @@ type
304301
# interleaved runs; the pre-hint policy is 75 ms), which is not worth 4x
305302
# the program — and the native boot pays for it twice, in 55.4s against
306303
# 28.7s.
307-
foreign: Table[string, ref ForeignModule]
308-
# Cached cross-module bodies. `ref` so growing the table doesn't
309-
# invalidate cursors that point into a previously-fetched buffer.
304+
foreign: Table[string, ForeignModule]
305+
# Opened callee modules, by module suffix. A `ForeignModule` is a `ref`
306+
# that owns each decl it parsed, so the cursors `lookupBody` hands out
307+
# stay valid as this grows.
308+
noForeign: HashSet[string]
309+
# Module suffixes with no `.c.nif` to open (asked once, not per call).
310+
foreignInfo: Table[SymId, InlineInfo]
311+
# Cross-module policy verdicts, computed on first ask per callee.
310312
inProgress*: HashSet[SymId]
311313
# Currently-being-spliced procs. Recursive `.inline` (direct or
312314
# mutual) would otherwise cause the splice + re-tr loop in dce2 to
@@ -333,7 +335,9 @@ proc initInlinerCtx*(moduleSuffix: string; src: ptr TokenBuf;
333335
maxDepth: maxDepth,
334336
growthLeft: high(int),
335337
counterPrefix: counterPrefix,
336-
foreign: initTable[string, ref ForeignModule](),
338+
foreign: initTable[string, ForeignModule](),
339+
noForeign: initHashSet[string](),
340+
foreignInfo: initTable[SymId, InlineInfo](),
337341
inProgress: initHashSet[SymId]())
338342

339343
proc growthBudget*(bodySize: int): int =
@@ -371,14 +375,14 @@ proc applySmallTotalRule(buf: var TokenBuf; infos: var seq[(SymId, InlineInfo)])
371375
info.inlinable = true
372376

373377
proc indexProcBodies(buf: var TokenBuf; bodies: var Table[SymId, int];
374-
infos: var Table[SymId, InlineInfo]; ownModule: bool) =
375-
## Walks the top-level `(stmts …)` and records `(proc :sym …)` decls
376-
## by sym → byte offset into `buf`, along with each proc's `InlineInfo`,
377-
## computed right here from the body we are indexing (`computeInlineInfo`
378-
## walks it once — a linear pass over a buffer we just parsed anyway). No
379-
## pragma transport is involved, so own-module and foreign bodies go
380-
## through the identical policy, and the size that is scored is the size
381-
## of the exact body a splice would copy.
378+
infos: var Table[SymId, InlineInfo]) =
379+
## Walks the module's own top-level `(stmts …)` and records `(proc :sym …)`
380+
## decls by sym → byte offset into `buf`, along with each proc's
381+
## `InlineInfo`, computed right here from the body we are indexing
382+
## (`computeInlineInfo` walks it once — a linear pass over a buffer we just
383+
## parsed anyway). No pragma transport is involved: a foreign callee goes
384+
## through the same `computeInlineInfo` (`lookupInlineInfo`), so the size
385+
## that is scored is the size of the exact body a splice would copy.
382386
var n = beginRead(buf)
383387
var found: seq[(SymId, InlineInfo)] = @[]
384388
if n.stmtKind == StmtsS:
@@ -394,13 +398,13 @@ proc indexProcBodies(buf: var TokenBuf; bodies: var Table[SymId, int];
394398
# being rewritten has them all in view: a foreign module's internal count says
395399
# nothing about how often ITS callers reach for the proc (measured: applied to
396400
# foreign modules it turned every once-called 400-token helper into an
397-
# always-splice for the whole program).
398-
if ownModule: applySmallTotalRule(buf, found)
401+
# always-splice for the whole program). Hence foreign procs never get it.
402+
applySmallTotalRule(buf, found)
399403
for (sym, info) in found:
400404
if info.inlinable: infos[sym] = info
401405

402406
proc collectProcBodies*(c: var InlinerCtx) =
403-
indexProcBodies(c.src[], c.bodies, c.ownInfo, ownModule = true)
407+
indexProcBodies(c.src[], c.bodies, c.ownInfo)
404408

405409
proc findForeignFile(c: InlinerCtx; modul, ext: string): string =
406410
## Search the caller's dir first, then the parent — system modules
@@ -415,48 +419,62 @@ proc findForeignFile(c: InlinerCtx; modul, ext: string): string =
415419
if fileExists(parent): return parent
416420
return ""
417421

418-
proc loadForeign(c: var InlinerCtx; modul: string): bool =
419-
## Lazy-load a foreign module: its proc bodies *and* their inline
420-
## annotations come out of the same parse, so `lookupInlineInfo` and
421-
## `lookupBody` share one file per module.
422+
proc openedForeign(c: var InlinerCtx; modul: string): bool =
423+
## Opens the callee module `modul` into `c.foreign` on first use; false when
424+
## there is no `.c.nif` for it.
422425
##
423426
## The `.c.nif`, because only this pass runs that late: it is post-DCE, so
424427
## its generic instances already name the module that won the merge and a
425428
## body copies into any other module unchanged. The `.x.nif` still names the
426429
## callee's own module for instances the merge later moves elsewhere; that
427430
## is the intra-module pass's world, not this one's.
428-
if modul == c.moduleSuffix: return true
429431
if modul in c.foreign: return true
432+
if modul in c.noForeign: return false
430433
let xpath = findForeignFile(c, modul, ".c.nif")
431-
if xpath.len == 0: return false
432-
var fm: ref ForeignModule
433-
new fm
434-
fm.buf = parseFromFile(xpath)
435-
indexProcBodies(fm.buf, fm.bodies, fm.inlineInfo, ownModule = false)
434+
if xpath.len == 0:
435+
c.noForeign.incl modul
436+
return false
437+
let fm = openForeignModule(xpath)
438+
if not fm.hasEmbeddedIndex:
439+
quit "inter-module inliner: " & xpath & " carries no embedded index"
436440
c.foreign[modul] = fm
437441
result = true
438442

443+
proc foreignProc(c: var InlinerCtx; calleeSym: SymId; decl: var Cursor): bool =
444+
## Parses the ONE `(proc …)` decl of `calleeSym` out of its module, through
445+
## the module's embedded index. This used to parse the whole callee module
446+
## and score every proc in it — including just to learn that the callee is
447+
## not inlinable. Measured on a native release build of nimsem: 2,620
448+
## whole-module parses, 393 MB, 3.83 s of 10.5 s of optimizer time, for at
449+
## most 11.8 % of the tokens ever being used; `std/syncio` alone was parsed
450+
## 88 times for 51 body fetches.
451+
let modul = pool.symModule(calleeSym)
452+
if not openedForeign(c, modul): return false
453+
let fm = c.foreign.getOrQuit(modul)
454+
let key = pool.symString(calleeSym)
455+
if not fm.hasDecl(key): return false
456+
# Interned into the global pool/tags so the SymIds and tag ids line up with
457+
# the module being rewritten; dense, seeded line info because a splice copies
458+
# it into the output.
459+
decl = fm.getDecl(key, globalTags, pool, withLineInfo = true)
460+
result = decl.stmtKind == ProcS
461+
439462
proc lookupBody(c: var InlinerCtx; calleeSym: SymId; outCur: var Cursor): bool =
440463
## Resolves a callee sym to a cursor pointing at its `(proc …)` decl.
441464
## The cursor's refcount keeps the underlying buffer alive for as
442-
## long as the cursor is held — `c.foreign` stores `ref
443-
## ForeignModule`, so subsequent table growth can't move the
444-
## TokenBuf out from under us. Returns false when we don't have a
445-
## body for `calleeSym` (extern decl, missing `.x.nif`, etc.).
465+
## long as the cursor is held; a foreign decl's buffer is owned by its
466+
## `ForeignModule` besides. Returns false when we don't have a body for
467+
## `calleeSym` (extern decl, missing `.c.nif`, etc.).
446468
let modul = pool.symModule(calleeSym)
447469
if modul == c.moduleSuffix:
448470
if calleeSym in c.bodies:
449471
outCur = cursorAt(c.src[], c.bodies.getOrQuit(calleeSym))
450472
return true
451473
return false
452-
if not loadForeign(c, modul): return false
453-
let fm = c.foreign.getOrQuit(modul)
454-
if calleeSym notin fm.bodies: return false
455474
# No further vetting here: the only bodies that reach this point already
456475
# passed `shouldInlineCall`, i.e. the size-driven policy in
457476
# `computeInlineInfo` (tiny → always, big → scored, `.noinline` → never).
458-
outCur = cursorAt(fm.buf, fm.bodies.getOrQuit(calleeSym))
459-
result = true
477+
result = foreignProc(c, calleeSym, outCur)
460478

461479
proc freshSym(c: var InlinerCtx; orig: SymId): SymId =
462480
## Mint a fresh local sym for an inlined body's local. The name must carry
@@ -487,9 +505,15 @@ proc lookupInlineInfo(c: var InlinerCtx; calleeSym: SymId): InlineInfo =
487505
let modul = pool.symModule(calleeSym)
488506
if modul == c.moduleSuffix:
489507
return c.ownInfo.getOrDefault(calleeSym, DefaultInlineInfo)
490-
if not loadForeign(c, modul): return DefaultInlineInfo
491-
result = c.foreign.getOrQuit(modul).inlineInfo.getOrDefault(calleeSym,
492-
DefaultInlineInfo)
508+
if calleeSym in c.foreignInfo: return c.foreignInfo.getOrQuit(calleeSym)
509+
var decl = default(Cursor)
510+
result = DefaultInlineInfo
511+
if foreignProc(c, calleeSym, decl):
512+
# Scored on its own body only — never the small-total rule, see
513+
# `indexProcBodies`.
514+
let info = computeInlineInfo(decl)
515+
if info.inlinable: result = info
516+
c.foreignInfo[calleeSym] = result
493517

494518
proc argContainsConstructor(callNode: Cursor): bool =
495519
## `(oconstr/aconstr …)` anywhere in an argument. The C backend renders an

src/lengc/nifmodules.nim

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,18 @@ proc densify(dest: var TokenBuf; n: var Cursor; cur: var NifLineInfo;
314314
of ExtendedSuffix, LineInfoLit, UnknownToken, EofToken, ParLe, ParRi:
315315
inc n # absorbed into the head token's own value/info; never freestanding
316316

317-
proc load*(filename: string): MainModule =
317+
proc load*(filename: string; pool: Pool = nil; tags: TagPool = nil): MainModule =
318318
## Load the main module, sniffing the file header for the actual format:
319319
## filenames stay `.nif` throughout the pipeline, the content decides.
320320
## Text is parsed with a canonical Leng tag pool so interned TagIds equal the
321321
## master ordinals that `stmtKind`/`typeKind`/`symKind` decode against; a
322322
## `.bif` is loaded zero-copy with its own fresh pools (the bif INVARIANT)
323323
## and translated to the canonical pools during the densify copy below.
324+
##
325+
## `pool`/`tags`: intern into these instead of fresh ones, so the module's
326+
## SymIds line up with buffers the caller already holds (shoggoth optimizes a
327+
## buffer the inter-module inliner produced in nifpools' global pools). `tags`
328+
## must be seeded by master ordinal, as `createLengTagPool` does.
324329
let fromBif = isBifFile(filename)
325330
var raw = default(TokenBuf)
326331
if fromBif:
@@ -333,7 +338,8 @@ proc load*(filename: string): MainModule =
333338
of rd.WrongHeader: quit "nif files must start with Version directive"
334339
of rd.WrongMeta: quit "the format of meta information is wrong!"
335340
let nodeCount = rd.fileSize(r) div 7
336-
raw = createTokenBuf(nodeCount, nil, createLengTagPool())
341+
raw = createTokenBuf(nodeCount, pool,
342+
if tags != nil: tags else: createLengTagPool())
337343
nifcoreparse.parse(r, raw)
338344
rd.close(r)
339345
# Densify line info so `info(n)` is valid at every node (see `densify`). The
@@ -345,7 +351,8 @@ proc load*(filename: string): MainModule =
345351
prog: NifProgram(scheme: splitModulePath(filename)))
346352
var remap = default(DensifyRemap)
347353
if fromBif:
348-
result.src = createTokenBuf(raw.len, nil, createLengTagPool())
354+
result.src = createTokenBuf(raw.len, pool,
355+
if tags != nil: tags else: createLengTagPool())
349356
remap = buildDensifyRemap(result.src, raw)
350357
else:
351358
result.src = createTokenBuf(raw.len, raw.pool, raw.tags)

src/lengc/shoggoth/imi_bridge.nim

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@
77
# distribution, for details about the copyright.
88
#
99

10-
## Lets the nifcore `optdriver` reuse the existing inter-module inliner
11-
## (`intermodinliner` → hexer's 910-line `intramodinliner`) without forking it
12-
## to nifcore. This module lives entirely in the **nifcursors** world (built
13-
## `-d:virtualParRi`, like `shoggoth.nim`); it exposes a single **string→string**
14-
## entry point so the two NIF APIs never share a `Cursor`/`TokenBuf` type across
15-
## the boundary — only serialized NIF text crosses.
16-
##
17-
## The cost is one parse→inline→serialize round-trip; `optdriver` then reparses
18-
## the result into nifcore for the per-body passes.
10+
## Lets the nifcore `optdriver` run the inter-module inliner
11+
## (`intermodinliner` → hexer's `intramodinliner`). The inliner is written
12+
## against the `nifprelude`/`nifpools` surface — nifcore plus the process-global
13+
## `pool`/`globalTags` — which `optdriver` does not import, so this module is
14+
## the one place the two surfaces meet. Both are nifcore underneath: the
15+
## `TokenBuf` itself crosses, interned in nifpools' global pools, and
16+
## `optdriver` builds its type context on those same pools.
1917

2018
import std / assertions
2119
include "../../lib" / nifprelude
2220
import nifpools
2321
import intermodinliner # runInterModuleInliner (nifpools)
2422

25-
proc runImi*(input, suffix, xnifDir: string; changed: var bool): string =
26-
## Parse the `.c.nif` at `input`, run inter-module inlining, and return the
27-
## resulting module as a **header-less** canonical NIF string (line info kept,
28-
## symbols fully expanded) for nifcore to reparse. `changed` reports whether
29-
## the inliner altered anything.
30-
var buf = parseFromFile(input, 4000)
31-
changed = runInterModuleInliner(buf, suffix, xnifDir)
32-
result = toString(buf)
23+
proc parseModule*(input: string): TokenBuf =
24+
## Parse the `.c.nif` at `input` into nifpools' global pools, with dense line
25+
## info (what the inliner reads; `optdriver` hands its passes a sparse copy).
26+
result = parseFromFile(input, 4000)
27+
28+
proc runImi*(input, suffix, xnifDir: string; changed: var bool): TokenBuf =
29+
## Parse the `.c.nif` at `input` and run inter-module inlining on it.
30+
## `changed` reports whether the inliner altered anything.
31+
result = parseModule(input)
32+
changed = runInterModuleInliner(result, suffix, xnifDir)

src/lengc/shoggoth/optdriver.nim

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import cse # runCSE + collectFunctionSummari
2525
import scalarizer # runScalarize (object → field scalars / SROA)
2626
import copyprop # runCopyProp (copy prop + dead-store elim)
2727
import unswitch # runUnswitch (loop unswitching)
28-
import imi_bridge # runImi (inter-module inliner, via nifcursors)
28+
import imi_bridge # runImi/parseModule (inter-module inliner)
2929
import vectorizer # runVectorizer (map loops -> (instr ...))
3030
export VecMode # the driver flag's type, for shoggoth.nim
3131
import vmrewriter # the DFA rewrite engine (arith.rewrite.nif)
@@ -243,19 +243,25 @@ proc processFile*(input, output: string; verify = false;
243243
## master NIFC tag ordinals (`stmtKind`/`takeProcDecl` rely on it).
244244
let suffix = extractModuleSuffix(input)
245245
var st = Stats()
246-
# 1. Whole-module inter-module inlining runs first, in the nifcursors world
247-
# (via the bridge); the result comes back as a NIF string.
246+
# 1. Whole-module inter-module inlining runs first (via the bridge, which
247+
# owns the nifpools surface the inliner is written against).
248248
var imiChanged = false
249-
let imiNif =
250-
if passOn("imi"): runImi(input, suffix, splitFile(input).dir, imiChanged)
251-
else: readFile(input)
249+
var src = block:
250+
var inlined =
251+
if passOn("imi"): runImi(input, suffix, splitFile(input).dir, imiChanged)
252+
else: parseModule(input)
253+
# The inliner works on dense line info; the per-body passes were calibrated
254+
# on the sparse form a plain parse stores (unswitch bounds loops in raw
255+
# tokens, suffixes included), and CSE's temp order follows token positions.
256+
# An in-memory copy keeps them seeing exactly the tokens they always saw.
257+
withSparseLineInfo(inlined)
252258
if imiChanged: inc st.intermodChanged
253-
# 2. Load the module as a typenav context (for type-precise aliasing), and
254-
# reparse the (post-inlining) body into nifcore SHARING that context's pool
255-
# so symbol ids line up between the type context and the optimization buffer.
256-
var typeCtx = load(input)
257-
var src = parseFromBuffer(imiNif, suffix, 4000,
258-
sharedPool = typeCtx.pool, sharedTags = typeCtx.tags)
259+
# 2. Load the module as a typenav context (for type-precise aliasing), in the
260+
# SAME pools as `src`, so symbol ids line up between the type context and
261+
# the optimization buffer. This used to serialize the inliner's result to
262+
# NIF text and re-parse it into the context's pools: on `sem.nim` that
263+
# round trip was 9.7 % of the whole run.
264+
var typeCtx = load(input, src.pool, src.tags)
259265
# The rewrite engine shares the module's pool/tags so its compiled patterns'
260266
# tag ids coincide with the buffers it rewrites.
261267
var eng = newEngine(ArithRules, typeCtx.pool, typeCtx.tags)

0 commit comments

Comments
 (0)