mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-01 19:33:42 +00:00
IC: dispatch the top-level walk on a tag id, not on ~20 tag names
`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
This commit is contained in:
@@ -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 <genericSym> <instSym> <genericParamsCount> <type>...) — 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 "<genericBodySym>" "<instType>") — 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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user