diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 1f2752561a..0881726bf9 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -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)) diff --git a/compiler/commands.nim b/compiler/commands.nim index 680ab8427e..0a045c2918 100644 --- a/compiler/commands.nim +++ b/compiler/commands.nim @@ -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}: diff --git a/compiler/deps.nim b/compiler/deps.nim index d9187ab248..d52d0df269 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -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::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" diff --git a/compiler/nifbackend.nim b/compiler/nifbackend.nim index 5caf89ca7d..611d3d0b44 100644 --- a/compiler/nifbackend.nim +++ b/compiler/nifbackend.nim @@ -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:`): + ## Backend lowering for this invocation's batch + ## (`--icBackendStage:lower --icBackendModules:`): ## enumerate the routines this module OWNS and write them to `.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:`): - ## 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:`): 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:`): + ## Backend emit for this invocation's batch + ## (`--icBackendStage:emit --icBackendModules:`): ## 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) diff --git a/compiler/options.nim b/compiler/options.nim index 145c97e1cf..e3107920a6 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -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] # (*)