IC: memoize the FileId -> FileIndex resolution, halving a cold --ic:on build

`oldLineInfo` resolved every decoded token's line info by copying a path out of
the buffer's filename pool and hashing it through `msgs.fileInfoIdx`. Every
time — no cache, on a step that runs once per node the decoder loads and once
per statement the generator emits.

On a 68-module target that is 259k calls at 5.2us. Memoized per pool it is
34ms, and the cold build goes from 21.5s to 10.1s.

`revTab` could not serve this: it is keyed by a `FileId` in the WRITER's global
`pool.files`, while a decoded token's `FileId` indexes the buffer's OWN
filename pool — a fresh one per `bif`-loaded module. So the cache is keyed by
(pool, FileId), as a `seq` because ids are small and dense within one pool.

`readPool` holds a REFERENCE rather than a raw pointer, and that is the whole
safety argument: it keeps the pool alive, so a freed pool cannot be replaced by
a new one at the same address and answer from the wrong file table.

Verified: `tests/ic` 39/39; the cursor-vs-`PNode` differential grinder clean
over ~150k graded nodes on `tools/icgrind` — and confirmed live by sabotaging
the pool-identity check, which makes it fire on `info` immediately; all 67
`.c` files an `--ic:on` build generates byte-identical to before; all 216 a
non-IC build generates likewise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMyRHByv7hhaQJ4Pa1bHbE
This commit is contained in:
araq
2026-08-30 20:11:36 +02:00
parent 8afd306b0d
commit db47363784

View File

@@ -130,6 +130,16 @@ type
revTab: Table[FileId, FileIndex] # reverse mapping for oldLineInfo
man: LineInfoManager
config: ConfigRef
# The READ direction's cache, which `revTab` cannot serve: `revTab` is keyed
# by a `FileId` in the WRITER's global `pool.files`, while a decoded token's
# `FileId` indexes the buffer's OWN filename pool. So the cache has to be
# keyed by (pool, FileId), and it is a `seq` because `FileId`s are small and
# dense within one pool. `readPool` holds a REFERENCE rather than a raw
# pointer on purpose: it keeps the pool alive, so a freed pool cannot be
# replaced by a new one at the same address and silently answer from the
# wrong file table.
readPool: Pool
readTab: seq[FileIndex]
proc newLineInfoWriter(config: ConfigRef): LineInfoWriter =
# `fileK` starts invalid so the one-entry cache never collides with a real
@@ -178,12 +188,26 @@ proc oldLineInfo(w: var LineInfoWriter; info: NifLineInfo; p: Pool): TLineInfo =
## it to a `TLineInfo`. `info.file` indexes the loaded buffer's OWN filename
## pool `p` (= `cursorPool(n)`), which is the shared `icPool` for a text-parsed
## module but a fresh per-file pool for a `bif`-loaded one.
##
## Memoized per pool. Resolving a name costs a string copy out of the pool
## plus a hash of a full path, and the generator asks for a node's line info
## on essentially every statement it emits — 259k times on a 68-module build,
## which was 1.36s of the 1.88s the cursor-driven generator spent.
if info.file == NoFile:
result = unknownLineInfo
else:
let filePath = p.filenames[info.file]
let fileIdx = msgs.fileInfoIdx(w.config, AbsoluteFile filePath)
result = TLineInfo(line: info.line.uint16, col: info.col.int16, fileIndex: fileIdx)
if p != w.readPool:
w.readPool = p
w.readTab = @[]
let id = int(uint32(info.file))
if id >= w.readTab.len:
let oldLen = w.readTab.len
w.readTab.setLen(id + 1)
for i in oldLen ..< w.readTab.len: w.readTab[i] = astli.InvalidFileIdx
if w.readTab[id] == astli.InvalidFileIdx:
w.readTab[id] = msgs.fileInfoIdx(w.config, AbsoluteFile p.filenames[info.file])
result = TLineInfo(line: info.line.uint16, col: info.col.int16,
fileIndex: w.readTab[id])
# ------------- Writer ---------------------------------------------------------------