mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-01 19:33:42 +00:00
IC: let one backend process handle a batch of modules
The plumbing behind the routing fix. `--icBackendModules:<a,b,c>` gives
the lower/cg/emit stages a LIST, `loadDepClosure` loads the batch's union
closure once instead of once per module, each stage loops over its
members, and `deps.nim` emits one nifmake rule per batch declaring all
its members' outputs. `-d:icBatchSize:N` is the dial; `0` means one batch
per job.
The default is 1, which reproduces the per-module fan-out exactly: `.c`,
`.c.nif` AND `.t.bif` byte-identical to the previous commit, ic 40/40,
`koch boot -d:release` equal executables.
Turning the dial up found three real bugs, none of which could exist
while a process wrote one TU:
* `loadDepClosure` deduplicated batch members against `visited`, which
already contains system — so a batch whose member IS system loaded
nothing and produced no artifact at all. System is an ordinary live
node with its own `.t.bif`/`.c.nif`; members now have their own set.
* `cg` finished each member's TU as it went. `finishModule` closes a TU,
and a later member's codegen routes definitions INTO an earlier
member's TU, which then silently dropped them. Generate every member,
then finish every member.
* `emitsBodyInThisModule(m, prc)` was asked with the DEMANDING module
where it means the module the body goes INTO. Identical while
`findPendingModule` always returned `m`; with a batch the definition
was marked declared in its owner's TU and then emitted by nobody — 18
undefined symbols at link.
Where it stands at batch size 4 and 8: builds, links, runs correctly, and
CPU drops from 9.8 s to 7.2 s / 6.2 s on a 67-module program. But the
artifacts are NOT invariant along the dial — 16 of 67 `.c` differ at 4 —
and the divergence is of two kinds. Most are the recorded gcc command
comment: per-module `{.passC.}` flags leak between batch members through
`writeBackendActions`. The rest is real: the set of minted type-bound
hooks moves (four `=destroy`/`=trace`/RTTI hooks vanish, one appears),
because which process mints a hook decides who owns it and batching
changes that. Duplication is also still 2.19x, unchanged — at these
sizes most demands are still for modules outside the batch.
So the dial stays at 1 until that invariant holds. It exists to be
turned, and it now reports what it finds when you do.
This commit is contained in:
@@ -253,11 +253,18 @@ proc bodyIsSeededByItsOwner(prc: PSym): bool =
|
||||
ownsRuntimeRoutine(prc, prc.itemId.module)
|
||||
|
||||
proc emitsBodyInThisModule(m: BModule, prc: PSym): bool =
|
||||
## Per-module backend codegen is concerned with ONE module: it emits the
|
||||
## bodies whose owner is this module and only *prototypes* a body some other
|
||||
## module's `cg` process is going to emit. The funnel where the main module
|
||||
## re-emitted its entire transitive closure (~1.8 GB, a 56 MB `.c.nif`) is
|
||||
## exactly this rule being absent.
|
||||
## Whether the translation unit `m` emits `prc`'s BODY, as opposed to only a
|
||||
## prototype for a body some other `cg` process emits. The funnel where the
|
||||
## main module re-emitted its entire transitive closure (~1.8 GB, a 56 MB
|
||||
## `.c.nif`) is exactly this rule being absent.
|
||||
##
|
||||
## `m` is the TU the body would go INTO — `findPendingModule`'s answer — not
|
||||
## the one that demanded it. The two were the same module for as long as a `cg`
|
||||
## process wrote exactly one TU, and asking with the demander was harmless.
|
||||
## With a batch they differ, and asking with the demander is the bug: a
|
||||
## definition routed to its owner inside the batch was marked declared there
|
||||
## and then emitted by nobody, since the demander is not the owner and the
|
||||
## owner never gets asked again (18 undefined symbols at link, batch size 4).
|
||||
##
|
||||
## The decision is a lookup against `bodyIsSeededByItsOwner`, i.e. against the
|
||||
## very predicates that drive the seeding, rather than a re-derivation from
|
||||
@@ -2485,7 +2492,8 @@ proc genProcLvl2(m: BModule, prc: PSym) =
|
||||
# which will actually become a function pointer
|
||||
if isReloadable(m, prc):
|
||||
genProcPrototype(q, prc)
|
||||
if emitsBodyInThisModule(m, prc):
|
||||
# Ask about `q`, the TU the body goes into. Outside a batch `q` IS `m`.
|
||||
if emitsBodyInThisModule(q, prc):
|
||||
genProcLvl3(q, prc)
|
||||
else:
|
||||
fillProcLoc(m, son(prc.ast, namePos))
|
||||
|
||||
@@ -989,12 +989,16 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
conf.icBackendStage = arg
|
||||
of "icbackendmodule":
|
||||
# `nim nifc` only: the NIF module suffix the cg/emit stage operates on (see
|
||||
# options.icBackendModule).
|
||||
of "icbackendmodule", "icbackendmodules":
|
||||
# `nim nifc` only: the NIF module suffixes the lower/cg/emit stage operates
|
||||
# on, comma-separated — the invocation's batch (see
|
||||
# options.icBackendModules). The singular spelling is the same switch: a
|
||||
# one-module batch is what the per-module fan-out passes.
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
conf.icBackendModule = arg
|
||||
conf.icBackendModules = @[]
|
||||
for suffix in arg.split(','):
|
||||
if suffix.len > 0: conf.icBackendModules.add suffix
|
||||
of "import":
|
||||
expectArg(conf, switch, arg, pass, info)
|
||||
if pass in {passCmd2, passPP}:
|
||||
|
||||
@@ -1308,6 +1308,60 @@ proc computeLiveBackendNodes(c: DepContext): seq[bool] =
|
||||
let idx = c.processedModules.getOrDefault(c.toPair(p).modname, -1)
|
||||
if idx >= 0: stack.add idx
|
||||
|
||||
proc intDefine(conf: ConfigRef; name: string; fallback: int): int =
|
||||
## `-d:<name>:N` as an int, or `fallback` when unset or unparsable.
|
||||
result = fallback
|
||||
if isDefined(conf, name):
|
||||
try: result = parseInt(conf.symbols[name])
|
||||
except ValueError: result = fallback
|
||||
|
||||
proc backendBatchSize(conf: ConfigRef; liveCount: int): int =
|
||||
## How many modules share one backend process. 1 is the historical per-module
|
||||
## fan-out; larger batches amortise the process floor and the dependency
|
||||
## closure load (measured on a 67-module program: 7.6 ms of process startup
|
||||
## and ~10 ms of closure loading per child, against 3.5 ms of actual codegen).
|
||||
##
|
||||
## `-d:icBatchSize:N` pins it. The default is 1 — the plumbing is in place but
|
||||
## the policy is not yet validated. `-d:icBatchSize:0` means "one batch per
|
||||
## job", which is the shape a tuned default will take: enough batches to keep
|
||||
## every core busy and no more, since a batch beyond that only buys
|
||||
## amortisation at the price of parallelism.
|
||||
if not isDefined(conf, "icBatchSize"): return 1
|
||||
result = intDefine(conf, "icBatchSize", 1)
|
||||
if result == 0:
|
||||
let jobs =
|
||||
if isDefined(conf, "icNoParallel"): 1
|
||||
elif isDefined(conf, "icJobs"): max(1, intDefine(conf, "icJobs", 1))
|
||||
elif conf.numberOfProcessors > 0: conf.numberOfProcessors
|
||||
else: 1
|
||||
result = (liveCount + jobs - 1) div jobs
|
||||
result = max(1, result)
|
||||
|
||||
proc backendBatches(c: DepContext; live: seq[bool]): seq[seq[int]] =
|
||||
## Partition the live non-main nodes into batches of node indices. The main
|
||||
## module is never in one: it loads the whole program, so batching it with
|
||||
## anything defeats the memory bound the per-module split exists to give.
|
||||
##
|
||||
## Contiguous runs of `c.nodes`, which is import-traversal order, so a batch's
|
||||
## members tend to share dependencies and its union closure stays close to one
|
||||
## member's. A smarter partition (by closure overlap, or by the dirty set on an
|
||||
## incremental build) belongs here and nowhere else — every stage already takes
|
||||
## whatever grouping this returns.
|
||||
var liveIdx: seq[int] = @[]
|
||||
for i in 0 ..< c.nodes.len:
|
||||
if live[i] and c.nodes[i].id != 0: liveIdx.add i
|
||||
let size = backendBatchSize(c.config, liveIdx.len)
|
||||
result = @[]
|
||||
var i = 0
|
||||
while i < liveIdx.len:
|
||||
var batch: seq[int] = @[]
|
||||
var j = i
|
||||
while j < liveIdx.len and batch.len < size:
|
||||
batch.add liveIdx[j]
|
||||
inc j
|
||||
result.add batch
|
||||
i = j
|
||||
|
||||
proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string =
|
||||
## Per-module backend build file. One `nim_nifc` command template (the actual
|
||||
## stage/module switches ride in each rule's `(args …)`), then the stages of
|
||||
@@ -1426,17 +1480,39 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# frontend writes `.s.nif`s content-stably, so an interface change to a
|
||||
# dependency re-sems (and re-emits the `.s.nif` of) every transitive importer;
|
||||
# a module whose own `.s.nif` is unchanged genuinely needs no re-lowering.
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
let batches = backendBatches(c, live)
|
||||
template suffixList(batch: seq[int]): string =
|
||||
var acc = ""
|
||||
for k, idx in batch:
|
||||
if k > 0: acc.add ","
|
||||
acc.add c.nodes[idx].files[0].modname
|
||||
acc
|
||||
|
||||
for batch in batches:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:lower"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
inputStr c.semmedFile(node.files[0])
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
for idx in batch:
|
||||
inputStr c.semmedFile(c.nodes[idx].files[0])
|
||||
inputStr argsFile
|
||||
outputStr tFiles[i]
|
||||
for idx in batch:
|
||||
outputStr tFiles[idx]
|
||||
b.endTree()
|
||||
# The main module is its own rule in every stage: it loads the whole program.
|
||||
block:
|
||||
let i = 0
|
||||
if live[i]:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:lower"
|
||||
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
|
||||
inputStr c.semmedFile(c.nodes[i].files[0])
|
||||
inputStr argsFile
|
||||
outputStr tFiles[i]
|
||||
b.endTree()
|
||||
|
||||
# cg: one rule per module. Input is this module's OWN `.t.nif`. cg DOES read
|
||||
# its dependencies' `.t.nif`s at runtime (loadDepClosure), but ordering is
|
||||
@@ -1448,25 +1524,38 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
# emit-everywhere'd but does not own is dropped by `emit` regardless, so a
|
||||
# stale copy here is harmless. The main module additionally depends on every
|
||||
# other `.c.nif` (it reads their init/datInit metas to wire up NimMain).
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
for batch in batches:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:cg"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
inputStr tFiles[i]
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
for idx in batch:
|
||||
inputStr tFiles[idx]
|
||||
inputStr argsFile
|
||||
if node.id == 0:
|
||||
for idx in batch:
|
||||
outputStr cnifFiles[idx]
|
||||
# The module's C compile/link directives (`{.passL.}` etc.), recorded so
|
||||
# the `link` stage recovers them without loading the module graph. See
|
||||
# `replayer.writeBackendActions`.
|
||||
outputStr cFiles[idx] & BackendActionsExt
|
||||
b.endTree()
|
||||
block:
|
||||
let i = 0
|
||||
if live[i]:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:cg"
|
||||
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
|
||||
inputStr tFiles[i]
|
||||
inputStr argsFile
|
||||
for j in 0 ..< c.nodes.len:
|
||||
if c.nodes[j].id != 0 and live[j]:
|
||||
inputStr cnifFiles[j]
|
||||
outputStr cnifFiles[i]
|
||||
# The module's C compile/link directives (`{.passL.}` etc.), recorded so the
|
||||
# `link` stage recovers them without loading the module graph. See
|
||||
# `replayer.writeBackendActions`.
|
||||
outputStr cFiles[i] & BackendActionsExt
|
||||
b.endTree()
|
||||
outputStr cnifFiles[i]
|
||||
outputStr cFiles[i] & BackendActionsExt
|
||||
b.endTree()
|
||||
|
||||
# merge: read the live modules' `.c.nif`, write the ownership/liveness
|
||||
# decision. The list is handed over as a FILE (`LiveModulesFile`) because the
|
||||
@@ -1495,26 +1584,40 @@ proc generateBackendBuildFile(c: DepContext; forwardedArgs: seq[string]): string
|
||||
b.endTree()
|
||||
|
||||
# emit: render each module's `.c` from its `.c.nif` + the merge decision.
|
||||
for i, node in c.nodes:
|
||||
if not live[i]: continue
|
||||
for batch in batches:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:emit"
|
||||
b.addStrLit "--icBackendModule:" & node.files[0].modname
|
||||
# Inputs: this module's OWN `.c.nif` and the global merge decision. emit also
|
||||
# loads `.t.nif`s at runtime (getCFile/type resolution), but those are depth 1
|
||||
# and emit is past the merge barrier, so they always exist — no need to list
|
||||
# them. (emit still re-fires for every module whenever `merge` rewrites the
|
||||
# decision file; making that incremental is a separate concern.)
|
||||
inputStr cnifFiles[i]
|
||||
b.addStrLit "--icBackendModules:" & suffixList(batch)
|
||||
# Inputs: each member's OWN `.c.nif` and the global merge decision. emit
|
||||
# reads nothing else — it derives its output paths rather than loading a
|
||||
# module graph. (It still re-fires for every module whenever `merge` rewrites
|
||||
# the decision file; making that incremental is a separate concern — though
|
||||
# batching is what makes the re-fire cheap.)
|
||||
for idx in batch:
|
||||
inputStr cnifFiles[idx]
|
||||
inputStr mergeFile
|
||||
outputStr cFiles[i]
|
||||
# The freshness proof for this rule; see nifbackend.generateEmitStage. The
|
||||
# `.c` alone cannot serve: it is written OnlyIfChanged, so a rule that ran
|
||||
# and produced identical bytes looks exactly like a rule that never ran.
|
||||
outputStr cFiles[i] & ".stamp"
|
||||
for idx in batch:
|
||||
outputStr cFiles[idx]
|
||||
# The freshness proof for this rule; see nifbackend.generateEmitStage. The
|
||||
# `.c` alone cannot serve: it is written OnlyIfChanged, so a rule that ran
|
||||
# and produced identical bytes looks exactly like a rule that never ran.
|
||||
outputStr cFiles[idx] & ".stamp"
|
||||
b.endTree()
|
||||
block:
|
||||
let i = 0
|
||||
if live[i]:
|
||||
b.addTree "do"
|
||||
b.addIdent "nim_nifc"
|
||||
b.withTree "args":
|
||||
b.addStrLit "--icBackendStage:emit"
|
||||
b.addStrLit "--icBackendModules:" & c.nodes[i].files[0].modname
|
||||
inputStr cnifFiles[i]
|
||||
inputStr mergeFile
|
||||
outputStr cFiles[i]
|
||||
outputStr cFiles[i] & ".stamp"
|
||||
b.endTree()
|
||||
|
||||
# link: compile + link every emitted `.c` in one process.
|
||||
b.addTree "do"
|
||||
|
||||
@@ -225,24 +225,30 @@ proc loadBackendModules(g: ModuleGraph; mainFileIdx: FileIndex):
|
||||
discard setupNifBackendModule(g, precompSys.module)
|
||||
result = (modules, precompSys, nifFiles)
|
||||
|
||||
proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
proc loadDepClosure(g: ModuleGraph; targetSuffixes: seq[string]):
|
||||
tuple[modules: seq[PrecompiledModule], precompSys: PrecompiledModule,
|
||||
target: PrecompiledModule] =
|
||||
## Per-module `cg`/`emit` for a NON-main target: load system + the target
|
||||
## module + the target's transitive import closure ONLY — not the whole
|
||||
## program. This is the "process the one file it is passed" model (à la
|
||||
## Nimony's `hexer c file.nif`): the foreign symbols the target's codegen
|
||||
## demands are loaded lazily by `ast2nif.moduleId`, which opens any referenced
|
||||
## module's NIF index on first touch, so a body in a not-loaded module still
|
||||
## resolves. The closure is loaded as full `BModule`s only so that the
|
||||
## incidental `g.mods[pos]` accesses during codegen resolve; system's own
|
||||
## internal closure (allocators, locks, …) is included because a target's
|
||||
## emit-everywhere codegen can demand those without importing them directly.
|
||||
targets: seq[PrecompiledModule]] =
|
||||
## Per-module `lower`/`cg`/`emit` for a NON-main batch: load system + every
|
||||
## module in the batch + their transitive import closure ONLY — not the whole
|
||||
## program. This is the "process the files it is passed" model (à la Nimony's
|
||||
## `hexer c file.nif`): the foreign symbols a target's codegen demands are
|
||||
## loaded lazily by `ast2nif.moduleId`, which opens any referenced module's NIF
|
||||
## index on first touch, so a body in a not-loaded module still resolves. The
|
||||
## closure is loaded as full `BModule`s only so that the incidental
|
||||
## `g.mods[pos]` accesses during codegen resolve; system's own internal closure
|
||||
## (allocators, locks, …) is included because a target's emit-everywhere
|
||||
## codegen can demand those without importing them directly.
|
||||
##
|
||||
## The whole program is no longer loaded in this process, which is what bounds
|
||||
## per-process memory under nifmake's parallel fan-out (the main module's `cg`,
|
||||
## which still loads everything for NimMain's init list and the method
|
||||
## dispatchers, runs essentially alone since every other `.c.nif` precedes it).
|
||||
##
|
||||
## The batch is loaded as ONE closure: `resetForBackend`, the system load and
|
||||
## the closure walk happen once no matter how many targets share the process,
|
||||
## and a module in two targets' closures is loaded once. That amortization is
|
||||
## the reason batches exist — a per-module process spends far more time here
|
||||
## than it spends generating code.
|
||||
resetForBackend(g)
|
||||
var isKnownFile = false
|
||||
let systemFileIdx = registerNifSuffix(g.config, systemNifSuffix(g.config), isKnownFile)
|
||||
@@ -254,18 +260,30 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
var visited = initHashSet[string]()
|
||||
visited.incl systemNifSuffix(g.config)
|
||||
|
||||
# Only the target is codegen'd, so only it needs its full AST; the closure is
|
||||
# loaded interface-only (demanded bodies come lazily from the kept-open
|
||||
# streams), which is what keeps a per-module process light under parallel fan-out.
|
||||
var isKnown = false
|
||||
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
|
||||
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
|
||||
visited.incl targetSuffix
|
||||
|
||||
# Only the batch is codegen'd, so only it needs full ASTs; the surrounding
|
||||
# closure is loaded interface-only (demanded bodies come lazily from the
|
||||
# kept-open streams), which is what keeps the process light under fan-out.
|
||||
var targets: seq[PrecompiledModule] = @[]
|
||||
var stack: seq[ModuleSuffix] = @[]
|
||||
if target.module != nil:
|
||||
modules.add target
|
||||
for dep in target.deps: stack.add dep
|
||||
# Separate from `visited`, which exists to keep the closure walk off modules
|
||||
# already loaded. System is in `visited` from the start yet can perfectly well
|
||||
# BE a batch member — it is a live node with its own `.t.bif` and `.c.nif` —
|
||||
# and then it needs the full-AST load like any other member, on top of the
|
||||
# interface-only load above. Reusing `visited` to deduplicate members skipped
|
||||
# it and produced a batch with nothing in it.
|
||||
var claimed = initHashSet[string]()
|
||||
for targetSuffix in targetSuffixes:
|
||||
if claimed.containsOrIncl(targetSuffix): continue
|
||||
var isKnown = false
|
||||
let targetIdx = registerNifSuffix(g.config, targetSuffix, isKnown)
|
||||
let target = moduleFromNifFile(g, targetIdx, {LoadFullAst})
|
||||
targets.add target
|
||||
# A member that is also another member's dependency must keep its full AST,
|
||||
# so claim it before the closure walk can load it interface-only.
|
||||
visited.incl targetSuffix
|
||||
if target.module != nil:
|
||||
modules.add target
|
||||
for dep in target.deps: stack.add dep
|
||||
if precompSys.module != nil:
|
||||
for dep in precompSys.deps: stack.add dep
|
||||
while stack.len > 0:
|
||||
@@ -282,7 +300,7 @@ proc loadDepClosure(g: ModuleGraph; targetSuffix: string):
|
||||
discard setupNifBackendModule(g, m.module)
|
||||
if precompSys.module != nil:
|
||||
discard setupNifBackendModule(g, precompSys.module)
|
||||
result = (modules, precompSys, target)
|
||||
result = (modules, precompSys, targets)
|
||||
|
||||
proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule; suffix: string): PrecompiledModule =
|
||||
@@ -296,6 +314,18 @@ proc findTargetModule(g: ModuleGraph; modules: seq[PrecompiledModule];
|
||||
cachedModuleSuffix(g.config, FileIndex precompSys.module.position) == suffix:
|
||||
return precompSys
|
||||
|
||||
proc backendBatch(conf: ConfigRef; mainSuffix: string):
|
||||
tuple[members: seq[string], isMain: bool] =
|
||||
## The module suffixes this invocation processes, and whether it is the
|
||||
## main-module invocation. Main is never batched with anything else: it loads
|
||||
## the WHOLE program (NimMain's init list and the method dispatchers are
|
||||
## whole-program facts), so putting another module in with it would defeat the
|
||||
## bound on per-process memory that the per-module split exists to provide.
|
||||
let members = conf.icBackendModules
|
||||
result = (members: members,
|
||||
isMain: members.len == 0 or
|
||||
(members.len == 1 and members[0] == mainSuffix))
|
||||
|
||||
proc setNestedClosureBodies(g: ModuleGraph; idgen: IdGenerator; n: PNode;
|
||||
owner: PSym; seen: var IntSet) =
|
||||
## A closure routine nested in `owner` (the `:anonymous` proc lambda-lifting
|
||||
@@ -360,8 +390,12 @@ proc reownFromTwin(n: PNode; twin, s: PSym) =
|
||||
for i in 0 ..< n.safeLen:
|
||||
reownFromTwin(n[i], twin, s)
|
||||
|
||||
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
seenNested: var IntSet)
|
||||
|
||||
proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend lowering (`--icBackendStage:lower --icBackendModule:<suffix>`):
|
||||
## Backend lowering for this invocation's batch
|
||||
## (`--icBackendStage:lower --icBackendModules:<a,b,c>`):
|
||||
## enumerate the routines this module OWNS and write them to `<module>.t.nif`.
|
||||
## Eventually this transforms each owned routine once, in the owner's id space,
|
||||
## so `cg` reads the result instead of re-deriving it (re-derivation per
|
||||
@@ -374,34 +408,46 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## stage does.
|
||||
nifcBackendActive = true
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
var modules: seq[PrecompiledModule]
|
||||
var precompSys: PrecompiledModule
|
||||
var target: PrecompiledModule
|
||||
if targetIsMain:
|
||||
var targets: seq[PrecompiledModule]
|
||||
if batch.isMain:
|
||||
var nifFiles: seq[string]
|
||||
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"Cannot load NIF file for main module: " & toFullPath(g.config, mainFileIdx))
|
||||
return
|
||||
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
|
||||
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
|
||||
else:
|
||||
(modules, precompSys, target) = block:
|
||||
(modules, precompSys, targets) = block:
|
||||
icProfStart(tLoadClosure)
|
||||
let r = loadDepClosure(g, g.config.icBackendModule)
|
||||
let r = loadDepClosure(g, batch.members)
|
||||
icProfStop(tLoadClosure)
|
||||
r
|
||||
# ONE PSym graph for the whole batch, so the guard against transforming a
|
||||
# nested routine twice has to span it: two members reaching the same nested
|
||||
# closure would otherwise inject its destructors twice into the same `PSym`.
|
||||
# (In the one-module-per-process fan-out the two members are two processes
|
||||
# with two copies, and each injects once.)
|
||||
var seenNested = initIntSet()
|
||||
for target in targets:
|
||||
lowerOneModule(g, target, seenNested)
|
||||
|
||||
proc lowerOneModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
seenNested: var IntSet) =
|
||||
## Lower the routines `target` OWNS and write its `.t.bif`. One batch member.
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: module not found for suffix: " & g.config.icBackendModule)
|
||||
"per-module lowering: module not found for suffix")
|
||||
return
|
||||
let modPos = target.module.position
|
||||
let tb = BModuleList(g.backend).mods[modPos]
|
||||
if tb == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module lowering: no backend module for suffix: " & g.config.icBackendModule)
|
||||
"per-module lowering: no backend module for suffix: " &
|
||||
cachedModuleSuffix(g.config, FileIndex modPos))
|
||||
return
|
||||
# Transform every owned routine ONCE in this single process's id space and
|
||||
# re-serialize the ENTIRE module as a proper indexed NIF (`writeLoweredModule`)
|
||||
@@ -417,11 +463,13 @@ proc generateLowerStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# `transformBody`/lambda-lifting LIFTS the closure env's type-bound ops
|
||||
# (`=destroy` etc.) into `g.opsLog`; snapshot its length so we serialize exactly
|
||||
# the ops THIS stage created (not those loaded from `.s.nif`).
|
||||
# Per MEMBER, not per batch: each member's `.t.bif` must carry exactly the ops
|
||||
# ITS lowering lifted, the way its own process would have written them.
|
||||
let opsLogStart = g.opsLog.len
|
||||
# Shared across the owned loop so a nested routine reachable from more than one
|
||||
# owner is transformed + destructor-injected EXACTLY once (double injection
|
||||
# would emit two `=destroy`/`=copy` runs).
|
||||
var seenNested = initIntSet()
|
||||
# `seenNested` comes from the caller and spans the whole batch — see the
|
||||
# comment at its declaration. Within one module it already served to transform
|
||||
# + destructor-inject a nested routine reachable from more than one owner
|
||||
# EXACTLY once (double injection would emit two `=destroy`/`=copy` runs).
|
||||
for s in moduleSymbolStubs(ast.program, FileIndex modPos):
|
||||
if ownsRuntimeRoutine(s, modPos):
|
||||
# REUSE path (`icReuseSemLowering` ON): a routine already transformed during
|
||||
@@ -507,13 +555,19 @@ proc visitDep(suffix: string;
|
||||
let bm = bl.mods[pm.module.position]
|
||||
if bm != nil: ordered.add bm
|
||||
|
||||
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule)
|
||||
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule)
|
||||
|
||||
proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend codegen (`--icBackendStage:cg --icBackendModule:<suffix>`):
|
||||
## generate C for the single module named by `icBackendModule` and write only
|
||||
## its `.c.nif` artifact (no merge, no `.c` render, no cc/link — those are
|
||||
## separate nifmake rules).
|
||||
## Backend codegen for this invocation's batch
|
||||
## (`--icBackendStage:cg --icBackendModules:<a,b,c>`): generate C for each
|
||||
## member and write its `.c.nif` artifact (no merge, no `.c` render, no
|
||||
## cc/link — those are separate nifmake rules).
|
||||
##
|
||||
## `findPendingModule` routes every demand into the target (emit-everywhere).
|
||||
## `findPendingModule` routes a demand to its owner when the owner is in the
|
||||
## batch and into the demanding TU otherwise (emit-everywhere).
|
||||
##
|
||||
## A NON-main target loads only its own import closure (`loadDepClosure`); the
|
||||
## whole program is no longer pulled into every parallel `cg` process. The main
|
||||
@@ -523,12 +577,11 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# gate `newSymNode`'s lazy-type marking to this stage only (see astdef)
|
||||
nifcBackendActive = true
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
var modules: seq[PrecompiledModule]
|
||||
var precompSys: PrecompiledModule
|
||||
var target: PrecompiledModule
|
||||
if targetIsMain:
|
||||
var targets: seq[PrecompiledModule]
|
||||
if batch.isMain:
|
||||
var nifFiles: seq[string]
|
||||
(modules, precompSys, nifFiles) = loadBackendModules(g, mainFileIdx)
|
||||
if modules.len == 0:
|
||||
@@ -539,27 +592,61 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# MERGE stage recomputes the one program-wide live set across all `.c.nif`s.
|
||||
# Running a whole-program liveness pass over all ~260 NIFs in the main `cg`
|
||||
# would cost ~900 MB for a result the merge stage throws away.
|
||||
target = findTargetModule(g, modules, precompSys, g.config.icBackendModule)
|
||||
targets = @[findTargetModule(g, modules, precompSys, mainSuffix)]
|
||||
else:
|
||||
# No whole-program load, hence no whole-program DCE: the target emits its
|
||||
# No whole-program load, hence no whole-program DCE: each member emits its
|
||||
# full demanded closure and the merge stage drops what is globally dead.
|
||||
(modules, precompSys, target) = block:
|
||||
(modules, precompSys, targets) = block:
|
||||
icProfStart(tLoadClosure)
|
||||
let r = loadDepClosure(g, g.config.icBackendModule)
|
||||
let r = loadDepClosure(g, batch.members)
|
||||
icProfStop(tLoadClosure)
|
||||
r
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module codegen: module not found for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
for i, target in targets:
|
||||
if target.module == nil:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module codegen: module not found for suffix: " &
|
||||
(if i < batch.members.len: batch.members[i] else: mainSuffix))
|
||||
return
|
||||
|
||||
# Declare which modules this process writes a TU for. `findPendingModule`
|
||||
# reads it to route a demanded definition to its owner when the owner is one
|
||||
# of them, and into the current TU otherwise. One member today — the set is
|
||||
# what a batched `cg` grows, and what keeps its members' definitions in their
|
||||
# own TUs instead of in whichever one demanded them first.
|
||||
BModuleList(g.backend).icEmitted.incl target.module.position
|
||||
let bl = BModuleList(g.backend)
|
||||
# Declare which modules this process writes a TU for, BEFORE any code is
|
||||
# generated: `findPendingModule` consults the set on the very first demand, so
|
||||
# a member added later would have its definitions routed into whichever TU
|
||||
# asked first — which is precisely what the set exists to prevent.
|
||||
for target in targets:
|
||||
bl.icEmitted.incl target.module.position
|
||||
|
||||
# Generate EVERY member before finishing ANY of them. `finishModule` closes a
|
||||
# TU (`finalCodegenActions` puts it in `modulesClosed`), and a later member's
|
||||
# codegen routes definitions it does not own INTO an earlier member's TU — see
|
||||
# `findPendingModule`. Finishing as we went closed those TUs first, and the
|
||||
# definitions that arrived afterwards were silently dropped: 18 undefined
|
||||
# symbols at link, all of them `_u`-flagged uniques whose owner happened to
|
||||
# sort earlier in its batch.
|
||||
for target in targets:
|
||||
cgGenerateModule(g, target)
|
||||
for target in targets:
|
||||
cgFinishModule(g, target, modules, precompSys)
|
||||
|
||||
# Writes each batch member's `.c.nif` (every other loaded module's TU is empty,
|
||||
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
|
||||
cgenWriteModules(g.backend, g.config)
|
||||
|
||||
# Always leave a `.c.nif` for every member, even one whose module has no code
|
||||
# (a leaf library whose procs all emit into their users): the nifmake graph
|
||||
# declares a `.c.nif` output per member, so a missing one would re-fire the
|
||||
# rule forever. An empty artifact renders to an empty `.c`.
|
||||
for target in targets:
|
||||
let tb = bl.mods[target.module.position]
|
||||
if tb != nil:
|
||||
let artifact = getCFile(tb).string & ".nif"
|
||||
if not fileExists(artifact):
|
||||
writeCnifArtifact("", artifact,
|
||||
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
|
||||
moduleBase = $getSomeNameForModule(tb))
|
||||
|
||||
proc cgGenerateModule(g: ModuleGraph; target: PrecompiledModule) =
|
||||
## Generate ONE batch member's code. Does NOT finish its TU — see the caller.
|
||||
# The `lower` stage already wrote each module's transformed bodies + lifted
|
||||
# hooks into its `.t.nif`, which the loaders above read directly (toNifFilename
|
||||
# resolves the `.t.nif`); transformed bodies arrive via loadSymFromCursor and
|
||||
@@ -570,10 +657,19 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# This module's top-level `var`s with a `=destroy` registered their teardown
|
||||
# in `graph.globalDestructors` during `genTopLevelStmt` above. Main's `cg` is
|
||||
# a different process and never sees them, so emit them as this TU's own
|
||||
# exported proc and announce the name in the meta head.
|
||||
# exported proc and announce the name in the meta head. Stays HERE, in the
|
||||
# generate pass: it consumes the destructors this module just registered.
|
||||
let tbm = bl.mods[target.module.position]
|
||||
if tbm != nil:
|
||||
tbm.icGlobalDtorName = genIcModuleDestroyGlobals(g, tbm)
|
||||
|
||||
proc cgFinishModule(g: ModuleGraph; target: PrecompiledModule;
|
||||
modules: seq[PrecompiledModule];
|
||||
precompSys: PrecompiledModule) =
|
||||
## Close ONE batch member's translation unit, once every member of the batch
|
||||
## has generated. The artifact write is not here: `cgenWriteModules` is a
|
||||
## single whole-list operation the caller runs after the whole batch.
|
||||
let bl = BModuleList(g.backend)
|
||||
# The main module also owns the whole-program method dispatchers + NimMain.
|
||||
if sfMainModule in target.module.flags:
|
||||
emitMethodDispatchers(g)
|
||||
@@ -650,21 +746,6 @@ proc generateCgStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
writeBackendActions(g, target.module, target.topLevel,
|
||||
getCFile(tb).string & BackendActionsExt)
|
||||
|
||||
# Writes only the target's `.c.nif` (every other loaded module's TU is empty,
|
||||
# so `cgenWriteModules` emits no artifact for it). cc/link are NOT run here.
|
||||
cgenWriteModules(g.backend, g.config)
|
||||
|
||||
# Always leave a `.c.nif` for the target, even when the module has no code
|
||||
# (a leaf library whose procs all emit into their users): the per-module
|
||||
# nifmake graph declares one `.c.nif` output per `cg` rule, so a missing one
|
||||
# would re-fire the rule forever. An empty artifact renders to an empty `.c`.
|
||||
if tb != nil:
|
||||
let artifact = getCFile(tb).string & ".nif"
|
||||
if not fileExists(artifact):
|
||||
writeCnifArtifact("", artifact,
|
||||
semmedNif = toNifFilename(g.config, FileIndex target.module.position),
|
||||
moduleBase = $getSomeNameForModule(tb))
|
||||
|
||||
proc generateMergeStage(g: ModuleGraph) =
|
||||
## Per-module backend merge (`--icBackendStage:merge`): a pure artifact
|
||||
## operation, no module graph loaded. Reads every `.c.nif` the `cg` stages
|
||||
@@ -700,16 +781,19 @@ proc generateMergeStage(g: ModuleGraph) =
|
||||
" live: " & $decision.live.len & " defs: " & $decision.defs &
|
||||
" liveDefs: " & $decision.liveDefs & " owned: " & $decision.owners.len
|
||||
|
||||
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
|
||||
isMain: bool; decision: MergeDecision)
|
||||
|
||||
proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
## Per-module backend emit (`--icBackendStage:emit --icBackendModule:<suffix>`):
|
||||
## Backend emit for this invocation's batch
|
||||
## (`--icBackendStage:emit --icBackendModules:<a,b,c>`):
|
||||
## render the target module's final `.c` from its `.c.nif` and the merge
|
||||
## decision. Loads the target the same way `cg` does so `getCFile` returns the
|
||||
## identical path `cg` wrote to (the main module's source-vs-suffix aliasing in
|
||||
## particular); no codegen runs. A non-main target loads only its own closure
|
||||
## (`loadDepClosure`) so emit, like `cg`, stays bounded under parallel fan-out.
|
||||
let mainSuffix = cachedModuleSuffix(g.config, mainFileIdx)
|
||||
let targetIsMain = g.config.icBackendModule.len == 0 or
|
||||
g.config.icBackendModule == mainSuffix
|
||||
let batch = backendBatch(g.config, mainSuffix)
|
||||
# emit renders a module's final `.c` PURELY from its own `.c.nif` and the merge
|
||||
# decision (see `renderCFromArtifact` — text filtering, no AST is touched). It
|
||||
# used to load the target's whole transitive import closure as BModules solely
|
||||
@@ -722,20 +806,30 @@ proc generateEmitStage(g: ModuleGraph; mainFileIdx: FileIndex) =
|
||||
# path directly instead — the SAME pure computation `deps.nim.backendCFile`
|
||||
# uses to DECLARE this stage's output (`getCFile` == that formula) — so an emit
|
||||
# process loads nothing and the fire-all costs process-startup, not a graph load.
|
||||
# The decision is read ONCE for the batch: it is a whole-program artifact, and
|
||||
# re-reading it per member was a per-process cost the batch exists to remove.
|
||||
let decision = readMergeDecision(getNimcacheDir(g.config).string / MergeDecisionFile)
|
||||
if decision.broken:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing or unparsable merge decision " & MergeDecisionFile)
|
||||
return
|
||||
let members = if batch.isMain: @[mainSuffix] else: batch.members
|
||||
for member in members:
|
||||
emitOneModule(g, mainFileIdx, member, batch.isMain, decision)
|
||||
|
||||
proc emitOneModule(g: ModuleGraph; mainFileIdx: FileIndex; member: string;
|
||||
isMain: bool; decision: MergeDecision) =
|
||||
## Render ONE batch member's final `.c` from its `.c.nif` and the batch's
|
||||
## merge decision.
|
||||
let cfilename =
|
||||
if targetIsMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
|
||||
else: AbsoluteFile g.config.icBackendModule
|
||||
if isMain: AbsoluteFile toFullPath(g.config, mainFileIdx)
|
||||
else: AbsoluteFile member
|
||||
let cfile = changeFileExt(completeCfilePath(g.config,
|
||||
mangleModuleName(g.config, cfilename).AbsoluteFile), icCFileExt(g.config)).string
|
||||
let artifact = cfile & ".nif"
|
||||
if not fileExists(artifact):
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing .c.nif artifact for suffix: " & g.config.icBackendModule)
|
||||
return
|
||||
let decision = readMergeDecision(getNimcacheDir(g.config).string / MergeDecisionFile)
|
||||
if decision.broken:
|
||||
rawMessage(g.config, errGenerated,
|
||||
"per-module emit: missing or unparsable merge decision " & MergeDecisionFile)
|
||||
"per-module emit: missing .c.nif artifact for suffix: " & member)
|
||||
return
|
||||
var dropped = 0
|
||||
let code = renderCFromArtifact(artifact, decision, extractFilename(artifact), dropped)
|
||||
|
||||
@@ -468,10 +468,16 @@ type
|
||||
# codegen+DCE+cc+link in one process). The stages
|
||||
# are wired as nifmake rules by `deps.nim`'s backend
|
||||
# build file. See `compiler/nifbackend.nim`.
|
||||
icBackendModule*: string # under `nim nifc` with icBackendStage in {cg,emit}:
|
||||
# the NIF module suffix this invocation codegens or
|
||||
# emits. The other modules are loaded only so types
|
||||
# resolve; their definitions are referenced extern.
|
||||
icBackendModules*: seq[string]
|
||||
# under `nim nifc` with icBackendStage in
|
||||
# {lower,cg,emit}: the NIF module suffixes this
|
||||
# invocation processes — its BATCH. One entry is
|
||||
# the per-module fan-out; several share one process
|
||||
# and therefore ONE dependency-closure load between
|
||||
# them, which is the whole point (see
|
||||
# `nifbackend.loadDepClosure`). Every other module
|
||||
# is loaded only so types resolve; its definitions
|
||||
# are referenced extern. Empty = the main module.
|
||||
spellSuggestMax*: int # max number of spelling suggestions for typos
|
||||
|
||||
cppDefines*: HashSet[string] # (*)
|
||||
|
||||
Reference in New Issue
Block a user