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:
araq
2026-08-31 10:53:14 +02:00
parent bad9d3b2bc
commit e5bba5a0fe
5 changed files with 347 additions and 132 deletions

View File

@@ -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))

View File

@@ -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}:

View File

@@ -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"

View File

@@ -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)

View File

@@ -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] # (*)