IC: performance/loading counters and bugfixes

This commit is contained in:
Araq
2026-07-01 22:32:34 +02:00
parent be0ce3f65c
commit 3a20ce0004
11 changed files with 211 additions and 7 deletions

View File

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

View File

@@ -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.} =

View File

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

View File

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

View File

@@ -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] =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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..<toInt(lengthOrd(conf, t)):
let n = toInt(lengthOrd(conf, t))
if n > 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..<n:
result.add getNullValue(c, elemType(t), info, conf)
of tyTuple:
result = newNodeIT(nkTupleConstr, info, t)
for a in t.kids: