From 1a70426a5b7d17bff8239ea9ebc6fa9cbf1740ea Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 30 Aug 2026 23:52:08 +0200 Subject: [PATCH] IC: dispatch the top-level walk on a tag id, not on ~20 tag names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `processTopLevel` decided what a top-level node was with an `elif` chain of about twenty `tagIs` calls, each of which resolves a tag NAME and compares strings. The two commonest outcomes — a real statement, and `implementation` — sit at the END of that chain, so the average node walked all of it. There are 1.46M such nodes on a 68-module build. Resolved once per tag id into a `TopTag` and dispatched with a `case`. The memo holds its `TagPool` by reference for the same reason `indexFromBif`'s and `bnode`'s do: that is what keeps a freed pool from being replaced at the same address and answering from the wrong table. processTopLevel 1314ms -> 1071ms loadDepClosure 2309ms -> 2126ms cold --ic:on build 8.92s -> 8.64s The last `elif` folded three conditions together (`LoadFullAst in flags` OR one of let/var/pragma); as a `case` that splits into the three tags, which always load, and `ttOther`, which loads only under `LoadFullAst`. Same truth table, now visible. A note on the measurement, because it nearly cost this change: the first timing of it read 11.35s against a remembered 8.91s. Re-running BOTH binaries back-to-back gave 8.914/8.931 for the old and 8.641/8.641 for the new — the first number was a `koch temp` build still settling. Compare against a baseline measured beside it, never against one from earlier. Verified: both configurations build; `tests/ic` 40/40; 67/67 generated `.c` byte-identical, cursor still identical to `PNode`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE --- compiler/ast2nif.nim | 141 ++++++++++++++++++++++++++++++++++--------- compiler/icprof.nim | 5 +- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index f32e1cb62b..4a65f7d5da 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -3996,6 +3996,80 @@ proc addReexportedEnumFields(c: var DecodeContext; sym: PSym; interf: var TStrTa if f != nil and f.kind == nkSym and f.sym != nil: strTableAdd(interf, f.sym) +type + TopTag = enum + ## Which top-level directive a tag names. `processTopLevel` used to decide + ## this with an `elif` chain of ~20 `tagIs` calls, i.e. up to twenty tag-NAME + ## string comparisons per node, and the common cases (a real statement, or + ## `implementation`) sit at the END of the chain so the average node walked + ## all of it — 1.46M nodes on a 68-module build. Resolved once per tag id + ## instead, and the chain becomes a `case`. + ttOther, ttReplay, ttUnusedId, ttModFlags, + ttRepConverter, ttRepDestroy, ttRepWasMoved, ttRepCopy, ttRepSink, ttRepDup, + ttRepTrace, ttRepDeepCopy, ttRepEnumToStr, ttRepMethod, ttRepPureEnum, + ttRepCppMember, ttExport, ttInclude, ttImport, ttReexpMod, ttOffer, ttTOffer, + ttModuleSrc, ttExpansion, ttSig, ttImplementation, + ttLetSection, ttVarSection, ttPragma + +const + letSectionTag = toNifTag(nkLetSection) + varSectionTag = toNifTag(nkVarSection) + pragmaTag = toNifTag(nkPragma) + +proc classifyTopTag(name: string): TopTag = + case name + of "replay": ttReplay + of "unusedid": ttUnusedId + of "modflags": ttModFlags + of "repconverter": ttRepConverter + of "repdestroy": ttRepDestroy + of "repwasmoved": ttRepWasMoved + of "repcopy": ttRepCopy + of "repsink": ttRepSink + of "repdup": ttRepDup + of "reptrace": ttRepTrace + of "repdeepcopy": ttRepDeepCopy + of "repenumtostr": ttRepEnumToStr + of "repmethod": ttRepMethod + of "reppureenum": ttRepPureEnum + of "repcppmember": ttRepCppMember + of "export": ttExport + of "include": ttInclude + of "import": ttImport + of "reexpmod": ttReexpMod + of "offer": ttOffer + of "toffer": ttTOffer + of "modulesrc": ttModuleSrc + of "expansion": ttExpansion + of "sig": ttSig + of "implementation": ttImplementation + else: + if name == letSectionTag: ttLetSection + elif name == varSectionTag: ttVarSection + elif name == pragmaTag: ttPragma + else: ttOther + +var topTagPool: TagPool = nil +var topTagCache: seq[int8] = @[] + ## `TagId -> TopTag`, -1 unresolved, for ONE tag pool. `topTagPool` holds the + ## pool by REFERENCE so it stays alive and a freed pool cannot be replaced at + ## the same address — the same argument `indexFromBif`'s and `bnode`'s memos + ## rest on. + +proc topTagAt(cur: Cursor): TopTag = + let pool {.cursor.} = cur.tags + if pool != topTagPool: + topTagPool = pool + topTagCache = @[] + let id = int(uint32(cursorTagId(cur))) + if id >= topTagCache.len: + let oldLen = topTagCache.len + topTagCache.setLen(id + 1) + for i in oldLen ..< topTagCache.len: topTagCache[i] = -1'i8 + if topTagCache[id] < 0: + topTagCache[id] = int8(ord(classifyTopTag(pool.tagName(cursorTagId(cur))))) + result = TopTag(topTagCache[id]) + proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag]; interf: var TStrTable; suffix: string; module: int): PrecompiledModule = ## Step 2 phase 2: walk the module body directly over the resident `buf` cursor @@ -4013,39 +4087,41 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] # `topLevel`. They sit in the module header before `(implementation)`. var cont = true while cont and cur.hasMore: + prof pTopNodes if cur.kind != TagLit: cont = false else: - if tagIs(cur, "replay"): + case topTagAt(cur) + of ttReplay: # Always load replay actions (macro cache operations) cur.into: while cur.hasMore: let replayNode = loadNode(c, cur, suffix, localSyms) if replayNode != nil: result.topLevel.sons.add replayNode - elif tagIs(cur, "unusedid"): + of ttUnusedId: # backend id seed — consumed eagerly by `moduleId`/`readUnusedId`; just # skip past it here so the rest of the header still loads. skip cur - elif tagIs(cur, "modflags"): + of ttModFlags: cur.into: if cur.hasMore and cur.kind == IntLit: result.moduleFlags = int32 intVal(cur) skip cur while cur.hasMore: skip cur - elif tagIs(cur, "repconverter"): loadLogOp(c, result.logOps, cur, ConverterEntry, attachedTrace, module) - elif tagIs(cur, "repdestroy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDestructor, module) - elif tagIs(cur, "repwasmoved"): loadLogOp(c, result.logOps, cur, HookEntry, attachedWasMoved, module) - elif tagIs(cur, "repcopy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedAsgn, module) - elif tagIs(cur, "repsink"): loadLogOp(c, result.logOps, cur, HookEntry, attachedSink, module) - elif tagIs(cur, "repdup"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDup, module) - elif tagIs(cur, "reptrace"): loadLogOp(c, result.logOps, cur, HookEntry, attachedTrace, module) - elif tagIs(cur, "repdeepcopy"): loadLogOp(c, result.logOps, cur, HookEntry, attachedDeepCopy, module) - elif tagIs(cur, "repenumtostr"): loadLogOp(c, result.logOps, cur, EnumToStrEntry, attachedTrace, module) - elif tagIs(cur, "repmethod"): loadLogOp(c, result.logOps, cur, MethodEntry, attachedTrace, module) - elif tagIs(cur, "reppureenum"): loadLogOp(c, result.logOps, cur, PureEnumEntry, attachedTrace, module) - elif tagIs(cur, "repcppmember"): loadLogOp(c, result.logOps, cur, CppMemberEntry, attachedTrace, module) - elif tagIs(cur, "export"): + of ttRepConverter: loadLogOp(c, result.logOps, cur, ConverterEntry, attachedTrace, module) + of ttRepDestroy: loadLogOp(c, result.logOps, cur, HookEntry, attachedDestructor, module) + of ttRepWasMoved: loadLogOp(c, result.logOps, cur, HookEntry, attachedWasMoved, module) + of ttRepCopy: loadLogOp(c, result.logOps, cur, HookEntry, attachedAsgn, module) + of ttRepSink: loadLogOp(c, result.logOps, cur, HookEntry, attachedSink, module) + of ttRepDup: loadLogOp(c, result.logOps, cur, HookEntry, attachedDup, module) + of ttRepTrace: loadLogOp(c, result.logOps, cur, HookEntry, attachedTrace, module) + of ttRepDeepCopy: loadLogOp(c, result.logOps, cur, HookEntry, attachedDeepCopy, module) + of ttRepEnumToStr: loadLogOp(c, result.logOps, cur, EnumToStrEntry, attachedTrace, module) + of ttRepMethod: loadLogOp(c, result.logOps, cur, MethodEntry, attachedTrace, module) + of ttRepPureEnum: loadLogOp(c, result.logOps, cur, PureEnumEntry, attachedTrace, module) + of ttRepCppMember: loadLogOp(c, result.logOps, cur, CppMemberEntry, attachedTrace, module) + of ttExport: if SkipInterfaceTables in flags: # Same reason the interface tables are skipped: `interf` is a scratch # table this caller throws away, so every `resolveSym` here (one per @@ -4053,10 +4129,12 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] # name-keyed `c.syms` cache that `resolveSym` refills lazily on a miss. skip cur continue + icProfStart(tExportBranch) cur.into: while cur.hasMore and cur.kind == DotToken: skip cur # flags / type while cur.hasMore: if cur.kind == Symbol: + prof pExportSyms let symAsStr = symName(cur) # Skip symbols re-exported by this dependency but owned by the module # being compiled fresh (they would collide with the fresh originals). @@ -4070,9 +4148,10 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] else: raiseAssert "expected Symbol or ParRi but got " & $cur.kind & " in export list of module " & suffix - elif tagIs(cur, "include"): loadInclude(c, cur, result.includes) - elif tagIs(cur, "import"): loadImport(c, cur, result.deps) - elif tagIs(cur, "reexpmod"): + icProfStop(tExportBranch) + of ttInclude: loadInclude(c, cur, result.includes) + of ttImport: loadImport(c, cur, result.deps) + of ttReexpMod: # a re-exported MODULE: (reexpmod "name" "suffix"); the module sym is a # qualifier in this module's interface — materialized by modulegraphs. var mname, msuffix = "" @@ -4081,7 +4160,7 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] if cur.hasMore and cur.kind == StrLit: (msuffix = strVal(cur); skip cur) if mname.len > 0 and msuffix.len > 0: result.reexportedModules.add (mname, msuffix) - elif tagIs(cur, "offer"): + of ttOffer: # (offer ...) — resolve # to PSyms/PTypes; modulegraphs registers them into `procInstCache`. # Best-effort: a type that fails to resolve drops the whole offer. @@ -4107,7 +4186,7 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] else: skip cur if ok and genSym != nil and instSym != nil: result.genericOffers.add (genSym, instSym, cts, paramsCount) - elif tagIs(cur, "toffer"): + of ttTOffer: # (toffer "" "") — intern the two full names, # resolve, FULLY load the instance (so `searchInstTypes` can match its # params). Best-effort: a failure to resolve drops the offer. @@ -4127,22 +4206,21 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] if genSym != nil and inst != nil: loadType(c, inst) result.typeOffers.add (genSym, inst) - elif tagIs(cur, "modulesrc"): + of ttModuleSrc: # self-identification record for the standalone include-graph scanner; # not needed by the loader, just skip past it. skip cur - elif tagIs(cur, "expansion"): + of ttExpansion: # template/macro expansion usage record for tooling (`idetools` scans it # as a `Symbol` use); the loader itself needs nothing from it. skip cur - elif tagIs(cur, "sig"): + of ttSig: # signature-symbol occurrence record for tooling (`idetools` scans it as a # `Symbol` use); the loader itself needs nothing from it. skip cur - elif tagIs(cur, "implementation"): + of ttImplementation: cont = false - elif LoadFullAst in flags or tagIs(cur, toNifTag(nkLetSection)) or - tagIs(cur, toNifTag(nkVarSection)) or tagIs(cur, toNifTag(nkPragma)): + of ttLetSection, ttVarSection, ttPragma: # Parse the full statement. let/var sections are loaded unconditionally # (see above) so `{.compileTime.}` globals reach the eager initializer. # Top-level pragmas are loaded too: a module-level `{.emit.}` (and the @@ -4152,8 +4230,13 @@ proc processTopLevel(c: var DecodeContext; cur: var Cursor; flags: set[LoadFlag] let stmtNode = loadNode(c, cur, suffix, localSyms) if stmtNode != nil: result.topLevel.sons.add stmtNode - else: - cont = false + of ttOther: + if LoadFullAst in flags: + let stmtNode = loadNode(c, cur, suffix, localSyms) + if stmtNode != nil: + result.topLevel.sons.add stmtNode + else: + cont = false proc registerModuleSelfSym*(c: var DecodeContext; suffix: string; m: PSym) = ## Bind the module's NIF name to the ONE module symbol the graph registered. diff --git a/compiler/icprof.nim b/compiler/icprof.nim index 2f2eadbfff..32fefcc0b3 100644 --- a/compiler/icprof.nim +++ b/compiler/icprof.nim @@ -31,11 +31,12 @@ when defined(icBNodeProf): ProfSlot* = enum pKind, pTagKindHit, pTagKindMiss, pAstChildren, pSkip, pSon, pLen, pLastSon, pIterYield, pSym, pTyp, pTypTagLit, pOrigin, pNilType, - pGenBodyCalls, pInfo, pIfaceExported, pIfaceHidden, pIfaceModules + pGenBodyCalls, pInfo, pIfaceExported, pIfaceHidden, pIfaceModules, + pTopNodes, pExportSyms TimeSlot* = enum tLoadClosure, tModuleId, tBifLoad, tPosIndex, tTopLevel, tInterfTables, tTransform, tHandOff, tGenBody, tAnalyses, - tSym, tTyp, tInfo, tOrigin + tSym, tTyp, tInfo, tOrigin, tExportBranch var profCounts: array[ProfSlot, int] var profNanos: array[TimeSlot, int64]