From 3a20ce0004dca03021852abe40ced5b80ffd2f7f Mon Sep 17 00:00:00 2001 From: Araq Date: Wed, 1 Jul 2026 22:32:34 +0200 Subject: [PATCH] IC: performance/loading counters and bugfixes --- compiler/ast2nif.nim | 117 +++++++++++++++++++++++++++++++++++++- compiler/astdef.nim | 11 ++++ compiler/ccgexprs.nim | 15 ++++- compiler/ccgtypes.nim | 6 +- compiler/ic/enum2nif.nim | 1 + compiler/modulegraphs.nim | 4 ++ compiler/options.nim | 2 +- compiler/semfold.nim | 7 ++- compiler/types.nim | 25 ++++++++ compiler/vm.nim | 20 +++++++ compiler/vmgen.nim | 10 +++- 11 files changed, 211 insertions(+), 7 deletions(-) diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index b1032e66d6..271cb0fa27 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -11,8 +11,10 @@ import std / [assertions, tables, sets] from std / strutils import startsWith, endsWith, contains -from std / os import fileExists, dirExists, walkFiles -from std / syncio import readFile +from std / os import fileExists, dirExists, walkFiles, existsEnv, + commandLineParams, getCurrentProcessId +from std / exitprocs import addExitProc +from std / syncio import readFile, stderr, writeLine from std / algorithm import sort import "../dist/checksums/src/checksums" / sha1 import astdef, idents, msgs, options @@ -912,6 +914,10 @@ proc writeNode(w: var Writer; dest: var IcBuilder; n: PNode; forAst = false) = if n == nil: dest.addDotToken else: + if nfLazyBody in n.flags and forceLazyBodyHook != nil: + # Materialize a deferred body before serializing so its real flags/typ and + # children are written (never the empty `nfLazyBody` placeholder). + forceLazyBodyHook(n) case n.kind of nkNone: assert n.typField == nil, "nkNone should not have a type" @@ -1767,8 +1773,18 @@ type semIndex: Table[string, NifIndexEntry] semTried: bool # `semBuf`/`semIndex` load attempted (idempotent) + PendingBody = object + ## A deferred routine body (bodyPos son). `cursor` points AT the body node in + ## the module buffer (kept alive by the cursor's refcounted owner); `localSyms` + ## is the snapshot of the enclosing sym def's local symbols so body-local + ## references resolve to the SAME PSyms the signature already created. + cursor: Cursor + thisModule: string + localSyms: Table[string, PSym] + DecodeContext* = object infos: LineInfoWriter + pendingBodies: Table[int, PendingBody] # nodeId(placeholder) -> deferred body #moduleIds: Table[string, int32] types: Table[string, (PType, NifIndexEntry)] syms: Table[string, (PSym, NifIndexEntry)] @@ -1778,11 +1794,60 @@ type ## Mangled module name of the module being compiled fresh (cmdM). Symbols ## belonging to it that are re-exported by a dependency must NOT be loaded ## as stubs, otherwise they collide with the freshly compiled originals. + symLoads, typeLoads: CountTable[FileIndex] + ## Diagnostics (opt-in via env `NIM_IC_LOADSTATS`): per OWNING-module count + ## of stub materializations in THIS process. Quantifies the "every backend + ## worker deserializes system.bif + a bunch of others" cost — breadth (how + ## many syms) attributed to duplication axis (which shared module). proc createDecodeContext*(config: ConfigRef; cache: IdentCache): DecodeContext = ## Supposed to be a global variable result = DecodeContext(infos: LineInfoWriter(config: config), cache: cache) +var loadStatsInit {.threadvar.}: int # 0=unknown 1=on 2=off +var statsCtxPtr {.threadvar.}: ptr DecodeContext +var loaderCtx {.threadvar.}: ptr DecodeContext # the live `program`; for lazy-body + # materialization off the len hook +var nodesDecoded {.threadvar.}: int # all PNodes materialized this proc +var astFieldNodes {.threadvar.}: int # subset: routine-body (s.ast) subtrees + +proc dumpLoadStatsExit() {.noconv.} = + if statsCtxPtr == nil: return + let c = statsCtxPtr + var merged = initTable[FileIndex, array[2, int]]() + for m, cnt in c.symLoads.pairs: merged.mgetOrPut(m, [0, 0])[0] = cnt + for m, cnt in c.typeLoads.pairs: merged.mgetOrPut(m, [0, 0])[1] = cnt + var order: seq[FileIndex] = @[] + var totS, totT: int = 0 + for m, a in merged: + order.add m + totS += a[0]; totT += a[1] + sort(order, proc (a, b: FileIndex): int = + (merged[b][0] + merged[b][1]) - (merged[a][0] + merged[a][1])) + let params = commandLineParams() + let target = if params.len > 0: params[^1] else: "?" + stderr.writeLine "=== IC loadstats pid=" & $getCurrentProcessId() & + " main=" & c.mainModuleSuffix & " target=" & target & " ===" + stderr.writeLine " TOTAL symLoads=" & $totS & " typeLoads=" & $totT & + " modulesTouched=" & $order.len + let pct = if nodesDecoded > 0: 100 * astFieldNodes div nodesDecoded else: 0 + stderr.writeLine " PNODES decoded=" & $nodesDecoded & " routineBody=" & + $astFieldNodes & " (" & $pct & "% deferrable via lazy PSym.ast)" + for m in order: + let a = merged[m] + let name = if c.mods.hasKey(m): c.mods[m].suffix else: "?" + stderr.writeLine " " & $(a[0] + a[1]) & "\tsym=" & $a[0] & " typ=" & $a[1] & + "\t" & name + +proc recordLoad(c: var DecodeContext; m: FileIndex; isType: bool) = + if loadStatsInit == 0: + loadStatsInit = if existsEnv("NIM_IC_LOADSTATS"): 1 else: 2 + if loadStatsInit == 1: + statsCtxPtr = addr c + addExitProc(dumpLoadStatsExit) + if loadStatsInit == 2: return + if isType: c.typeLoads.inc(m) else: c.symLoads.inc(m) + proc nextBackendSymItem*(c: var DecodeContext; module: int32): int32 = ## Allocate the next backend-minted SYM item for `module` from the SAME ## per-module counter the loader uses when it re-homes `@bk` syms loaded from @@ -2317,6 +2382,7 @@ proc loadTypeFromCursor(c: var DecodeContext; n: var Cursor; t: PType; localSyms proc loadType*(c: var DecodeContext; t: PType) = if t.state != Partial: return t.state = c.loadedState + recordLoad(c, t.itemId.module.FileIndex, isType = true) # A backend-minted (`@bk`) closure-env type produced by the `lower` stage lives # ONLY in the `.t.nif` and is keyed by its `@bk` name (see nifTypeName), not the # canonical `typeToNifSym` (which asserts non-`@bk`). Reconstruct that name so a @@ -2410,7 +2476,10 @@ proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule: s.ownerFieldImpl = loadSymStub(c, n, thisModule, localSyms) # Load the AST for routine symbols and constants # Constants need their AST for astdef() to return the constant's value + let astNodesBefore = nodesDecoded s.astImpl = loadNode(c, n, thisModule, localSyms) + if loadStatsInit == 1 and s.kindImpl in routineKinds: + astFieldNodes += nodesDecoded - astNodesBefore loadLoc c, n, s.locImpl s.constraintImpl = loadNode(c, n, thisModule, localSyms) s.instantiatedFromImpl = loadSymStub(c, n, thisModule, localSyms) @@ -2437,6 +2506,8 @@ proc loadSymFromCursor(c: var DecodeContext; s: PSym; n: var Cursor; thisModule: proc loadSym*(c: var DecodeContext; s: PSym) = if s.state != Partial: return s.state = c.loadedState + if loaderCtx == nil: loaderCtx = addr c + recordLoad(c, s.itemId.module.FileIndex, isType = false) let symsModule = s.itemId.module.FileIndex let nifname = globalName(s, c.infos.config) var n = cursorFromIndexEntry(c, symsModule, c.syms[nifname][1]) @@ -2489,6 +2560,7 @@ template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNod proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; localSyms: var Table[string, PSym]): PNode = + if loadStatsInit == 1: inc nodesDecoded result = nil case n.kind of Symbol: @@ -2677,6 +2749,27 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; of nkNilLit: c.withNode n, result, kind: discard + of routineDefs: + # Defer the heavy `bodyPos` son: build the routine-def header eagerly, but + # install a `nfLazyBody` placeholder (carrying the real body kind, so cheap + # `ast[bodyPos].kind != nkEmpty` checks need no load) whose children are + # materialized on demand (see `materializeLazyBody`, driven by the `len` + # hook). An empty body is a single node — not worth deferring. + c.withNode n, result, kind: + var idx = 0 + while n.hasMore: + if idx == bodyPos and n.kind == TagLit and + n.nodeKind notin {nkEmpty, nkNone}: + let info = c.infos.oldLineInfo(n.info, cursorPool(n)) + let ph = newNodeI(n.nodeKind, info) + ph.flags.incl nfLazyBody + c.pendingBodies[cast[int](ph)] = + PendingBody(cursor: n, thisModule: thisModule, localSyms: localSyms) + result.sons.add ph + skip n + else: + result.sons.add c.loadNode(n, thisModule, localSyms) + inc idx else: c.withNode n, result, kind: while n.hasMore: @@ -2684,6 +2777,26 @@ proc loadNode(c: var DecodeContext; n: var Cursor; thisModule: string; else: raiseAssert "expected string literal but got " & $n.kind +proc materializeLazyBody*(c: var DecodeContext; node: PNode) = + ## Fill a `nfLazyBody` placeholder's children in place (identity-preserving: + ## callers already hold `node`). Decodes the deferred body from the stashed + ## cursor with the enclosing def's `localSyms` so param/local refs resolve to + ## the SAME PSyms the signature created. + node.flags.excl nfLazyBody # clear first: the loadNode below calls `len` + let key = cast[int](node) + var pb = PendingBody() + if not c.pendingBodies.pop(key, pb): return + var cur = pb.cursor + let real = c.loadNode(cur, pb.thisModule, pb.localSyms) + # `real` has the same kind as the placeholder (peeked at defer time); graft its + # decoded content onto the node the callers hold. + node.sons = real.sons + node.typField = real.typField + node.flags = real.flags + +forceLazyBodyHook = proc (n: PNode) {.nimcall.} = + if loaderCtx != nil: materializeLazyBody(loaderCtx[], n) + proc loadSymFromIndexEntry(c: var DecodeContext; module: FileIndex; nifName: string; entry: NifIndexEntry; thisModule: string): PSym = ## Loads a symbol from the NIF index entry using the entry directly. diff --git a/compiler/astdef.nim b/compiler/astdef.nim index 63de1814fb..8817e3b15c 100644 --- a/compiler/astdef.nim +++ b/compiler/astdef.nim @@ -339,6 +339,9 @@ type # because openSym experimental switch is disabled # gives warning instead nfLazyType # node has a lazy type + nfLazyBody # IC: this node is a placeholder for a routine body (bodyPos son) + # not yet materialized. Reading its children (via `len`/`safeLen`) + # triggers `forceLazyBodyHook`. Process-local, stripped on serialize. TNodeFlags* = set[TNodeFlag] TTypeFlag* = enum # keep below 32 for efficiency reasons (now: 47) @@ -903,7 +906,15 @@ const defaultOffset* = -1 +var forceLazyBodyHook*: proc (n: PNode) {.nimcall.} + ## Set by the IC loader (ast2nif). When a node carries `nfLazyBody`, any access + ## to its children through `len` materializes the deferred routine body in place. + ## `safeLen` delegates to `len`, so it is covered transitively; a lazy body is + ## never a leaf kind, so the `{nkNone..nkNilLit}` short-circuit never hides it. + proc len*(n: PNode): int {.inline.} = + if nfLazyBody in n.flags and forceLazyBodyHook != nil: + forceLazyBodyHook(n) result = n.sons.len proc safeLen*(n: PNode): int {.inline.} = diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 171d656230..6b822dfa9d 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -3957,6 +3957,13 @@ proc getDefaultValue(p: BProc; typ: PType; info: TLineInfo; result: var Builder) let elemTyp = skipTypes(t.elementType, abstractRange+{tyOwned}-{tyTypeDesc}) if isOpaqueImportcType(elemTyp): result.add "{0}" + elif toInt(lengthOrd(p.config, t.indexType)) > broadcastArrayThreshold and + elemTyp.kind in {tyInt..tyUInt64, tyBool, tyChar, tyFloat..tyFloat128, + tyPtr, tyPointer, tyCstring}: + # Large array of a scalar whose default is the zero representation: a single + # C `{0}` zero-fills all `lengthOrd` slots instead of emitting that many + # initializers (keeps huge SSZ-style zero buffers compact in the C output). + result.add "{0}" else: var arrInit: StructInitializer result.addStructInitializer(arrInit, kind = siArray): @@ -4243,7 +4250,13 @@ proc genBracedInit(p: BProc, n: PNode; isConst: bool; optionalType: PType; resul var d: TLoc = initLocExpr(p, n) result.add rdLoc(d) of tyArray, tyVarargs: - genConstSimpleList(p, n, isConst, result) + if isDefaultBroadcastArray(n, p.config): + # Compact zero/null-default array (see `isDefaultBroadcastArray`): the + # whole thing is the null value of every slot, so a single C `{0}` + # zero-fills all `lengthOrd` elements — no need to materialise them. + result.add "{0}" + else: + genConstSimpleList(p, n, isConst, result) of tyTuple: genConstTuple(p, n, isConst, typ, result) of tyOpenArray: diff --git a/compiler/ccgtypes.nim b/compiler/ccgtypes.nim index 58343f2c44..91f87b5992 100644 --- a/compiler/ccgtypes.nim +++ b/compiler/ccgtypes.nim @@ -819,7 +819,11 @@ proc genRecordFieldsAux(m: BModule; n: PNode, # don't use fieldType here because we need the # tyGenericInst for C++ template support let noInit = sfNoInit in field.flags or (field.typ.sym != nil and sfNoInit in field.typ.sym.flags) - if not noInit and (fieldType.isOrHasImportedCppType() or hasCppCtor(m, field.owner.typ)): + # Under `nim ic`, object fields are local NIF syms restored without an + # `owner`; `rectype` is the owning record type, so fall back to it rather + # than deref a nil `field.owner`. + let ownerTyp = if field.owner != nil: field.owner.typ else: rectype + if not noInit and (fieldType.isOrHasImportedCppType() or hasCppCtor(m, ownerTyp)): var didGenTemp = false initializer = genCppInitializer(m, nil, fieldType, didGenTemp) result.addField(field, sname, typ, isFlexArray, initializer) diff --git a/compiler/ic/enum2nif.nim b/compiler/ic/enum2nif.nim index 4b7860fc96..6b0a809026 100644 --- a/compiler/ic/enum2nif.nim +++ b/compiler/ic/enum2nif.nim @@ -1473,6 +1473,7 @@ proc genFlags*(s: set[TNodeFlag]; dest: var string) = of nfSkipFieldChecking: dest.add "s0" of nfDisabledOpenSym: dest.add "d3" of nfLazyType: dest.add "l1" + of nfLazyBody: discard # process-local placeholder; never serialized proc parse*(t: typedesc[TNodeFlag]; s: string): set[TNodeFlag] = diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 0b238189c8..78a5f42ab3 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -906,6 +906,10 @@ proc needsCompilation*(g: ModuleGraph, fileIdx: FileIndex): bool = proc getBody*(g: ModuleGraph; s: PSym): PNode {.inline.} = result = s.ast[bodyPos] + if result != nil and nfLazyBody in result.flags and forceLazyBodyHook != nil: + # Sanctioned body-access gate (see astdef.bodyPos): materialize the deferred + # IC body so callers may safely touch `.sons` directly, not only via `len`. + forceLazyBodyHook(result) assert result != nil when not defined(nimKochBootstrap): diff --git a/compiler/options.nim b/compiler/options.nim index fc239beb63..864ef6e72d 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -29,7 +29,7 @@ const nimEnableCovariance* = defined(nimEnableCovariance) - icFormatVersion* = "26" + icFormatVersion* = "27" ## Version of the IC cache format (the sem-NIF module layout written by ## ast2nif.nim plus the iface/impl/edges side files). Bump it whenever ## that layout changes: `commandIc` wipes a nimcache whose `ic.version` diff --git a/compiler/semfold.nim b/compiler/semfold.nim index 42285618ff..371cf539f7 100644 --- a/compiler/semfold.nim +++ b/compiler/semfold.nim @@ -476,7 +476,12 @@ proc foldArrayAccess(m: PSym, n: PNode; idgen: IdGenerator; g: ModuleGraph): PNo #localError(g.config, n.info, formatErrorIndexBound(idx, x.len-1) & $n) of nkBracket: idx -= toInt64(firstOrd(g.config, x.typ)) - if idx >= 0 and idx < x.len: result = x[int(idx)] + if isDefaultBroadcastArray(x, g.config): + # compact default array: any in-bounds index folds to the default element + if idx >= 0 and idx < toInt64(lengthOrd(g.config, x.typ.skipTypes(abstractInst))): + result = copyTree(x[0]) + else: result = nil + elif idx >= 0 and idx < x.len: result = x[int(idx)] else: result = nil #localError(g.config, n.info, formatErrorIndexBound(idx, x.len-1) & $n) diff --git a/compiler/types.nim b/compiler/types.nim index f4c7e74bc5..b8e889a3cb 100644 --- a/compiler/types.nim +++ b/compiler/types.nim @@ -633,6 +633,31 @@ proc lengthOrd*(conf: ConfigRef; t: PType): Int128 = let first = firstOrd(conf, t) result = last - first + One +const broadcastArrayThreshold* = 32 + ## `getNullValue` represents the default of an `array[N, T]` with `N` above this + ## as a single *broadcast* element — a one-son `nkBracket` standing for `N` + ## identical zero copies — instead of materialising `N` zero nodes. This keeps + ## huge zeroed arrays (e.g. SSZ byte buffers in nimbus) compact in the IC caches + ## (`.s.bif`/`.t.bif`), in the VM, and in the generated C (`{0}` zero-fills). + +proc isDefaultBroadcastArray*(n: PNode; conf: ConfigRef): bool = + ## True iff `n` is a broadcast default array: one son that stands for + ## `lengthOrd` identical copies. A normal post-sem array literal always has + ## exactly `lengthOrd` sons, so `len == 1 < lengthOrd` is an unambiguous marker. + result = n != nil and n.kind == nkBracket and n.len == 1 and n.typ != nil and + n.typ.skipTypes(abstractInst).kind == tyArray and + lengthOrd(conf, n.typ.skipTypes(abstractInst)) > One + +proc expandBroadcastArray*(n: PNode; conf: ConfigRef) = + ## Materialise a broadcast default array (see `isDefaultBroadcastArray`) into a + ## full `lengthOrd`-son `nkBracket`, each son a copy of the single default + ## element. Used by VM ops that index-address, mutate, or measure such a node; + ## the common read-only paths leave it compact. + if isDefaultBroadcastArray(n, conf): + let total = toInt(lengthOrd(conf, n.typ.skipTypes(abstractInst))) + let elem = n[0] + for i in 1 ..< total: n.add copyTree(elem) + # -------------- type equality ----------------------------------------------- type diff --git a/compiler/vm.nim b/compiler/vm.nim index 23c579dd17..0d9ab99b2e 100644 --- a/compiler/vm.nim +++ b/compiler/vm.nim @@ -702,6 +702,10 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = # A bodge, but this takes in `toOpenArray(rb, rc, rc)` and emits # nkTupleConstr(x, y, z) into the `regs[ra]`. These can later be used for calculating the slice we have taken. decodeBC(rkNode) + # Slicing/openArray needs the real length and per-element nodes, so a + # compact default array must be materialised first. + if isDefaultBroadcastArray(regs[ra].node, c.config): + expandBroadcastArray(regs[ra].node, c.config) let collection = regs[ra].node leftInd = regs[rb].intVal @@ -770,6 +774,15 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = regs[ra].node.intVal = src.strVal[idx].ord else: stackTrace(c, tos, pc, formatErrorIndexBound(idx, src.strVal.len-1)) + elif isDefaultBroadcastArray(src, c.config): + # `a[i]` on a compact default array yields `default(T)` directly, without + # ever materialising the (potentially huge) array — the point of the + # broadcast form. See `getNullValue`/`isDefaultBroadcastArray`. + let total = toInt(lengthOrd(c.config, src.typ.skipTypes(abstractInst))) + if idx <% total: + regs[ra].node = copyTree(src[0]) + else: + stackTrace(c, tos, pc, formatErrorIndexBound(idx, total-1)) elif src.kind notin {nkEmpty..nkFloat128Lit} and idx <% src.len: regs[ra].node = src[idx] else: @@ -781,6 +794,9 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = stackTrace(c, tos, pc, formatErrorIndexBound(regs[rc].intVal, high(int))) let idx = regs[rc].intVal.int let src = if regs[rb].kind == rkNode: regs[rb].node else: regs[rb].nodeAddr[] + # Taking the address of an element needs distinct, stable per-slot nodes, so + # a compact default array must be materialised first. + if isDefaultBroadcastArray(src, c.config): expandBroadcastArray(src, c.config) case src.kind of nkTupleConstr: let @@ -829,6 +845,8 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = let idx = regs[rb].intVal.int assert regs[ra].kind == rkNode let arr = regs[ra].node + # Writing a slot materialises a compact default array into a full literal. + if isDefaultBroadcastArray(arr, c.config): expandBroadcastArray(arr, c.config) case arr.kind of nkTupleConstr: # refer to `opcSlice` let @@ -1031,6 +1049,8 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = case node.kind of nkTupleConstr: # refer to `of opcSlice` regs[ra].intVal = node[2].intVal - node[1].intVal + 1 - high + elif isDefaultBroadcastArray(node, c.config): + regs[ra].intVal = toInt(lengthOrd(c.config, node.typ.skipTypes(abstractInst))) - high else: # safeArrLen also return string node len # used when string is passed as openArray in VM diff --git a/compiler/vmgen.nim b/compiler/vmgen.nim index 79bbba7d44..118ea07ede 100644 --- a/compiler/vmgen.nim +++ b/compiler/vmgen.nim @@ -2029,8 +2029,16 @@ proc getNullValue(c: PCtx; typ: PType, info: TLineInfo; conf: ConfigRef): PNode getNullValueAux(c, t, t.n, result, conf, currPosition) of tyArray: result = newNodeIT(nkBracket, info, t) - for i in 0.. 0: result.add getNullValue(c, elemType(t), info, conf) + # For a large array, keep a single broadcast element (the default of every + # slot is identical) instead of `n` copies; `isDefaultBroadcastArray` + # consumers expand on demand. Small arrays stay fully materialised so the + # well-trodden paths are untouched. See `broadcastArrayThreshold`. + if n <= broadcastArrayThreshold: + for i in 1..