From c6f70374bc436249213a89f8ab3e2032d331a118 Mon Sep 17 00:00:00 2001 From: araq Date: Mon, 31 Aug 2026 19:59:09 +0200 Subject: [PATCH] IC: make `timed` arm the profiler, and price the C compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `timed` recorded into `profNanos` but never called `armProf()`, so a process whose only instrumentation is a `timed` never registered the exit dump and reported nothing at all. The `merge`, `emit` and `link` stages have been silently absent from every profile in this branch — including the one I used to claim the backend was fully accounted for. With them present, Atlas cold at batch size 16, parallel (9.66s elapsed): frontend 181 proc 10.60 s summed process wall lower 14 proc 4.49 s cg 14 proc 4.50 s merge 1 proc 0.20 s emit 14 proc 0.42 s link 1 proc 1.65 s <- the entire C compile + link That settles the C compiler as a target: its 12.2s of CPU is 1.65s of wall, because `callCCompiler` fans out across cores, and the excess over a whole-program build is ~0.4s of it. `bnode.nim` records where the excess is — 3.8MB of the 5.4MB is per-TU prototypes and typedefs, which is intrinsic to emitting 204 translation units instead of 139 — and why the obvious sub-target is not one either: 53 of the 204 object files define nothing, and compiling all 53 costs 0.23s of user time. Fewer, larger TUs is the only real fix, and it trades directly against what IC is for: a sandwich edit rebuilds exactly one `.c` and one `.o`. Behind `-d:icBNodeProf`. ic 42/42. --- compiler/bnode.nim | 27 +++++++++++++++++++++++++++ compiler/icprof.nim | 5 +++++ 2 files changed, 32 insertions(+) diff --git a/compiler/bnode.nim b/compiler/bnode.nim index 918abeef3a..3c5ffb1298 100644 --- a/compiler/bnode.nim +++ b/compiler/bnode.nim @@ -163,6 +163,33 @@ ## and build into a LOCAL table before assigning it back (loading symbols can ## grow `g.ifaces`, which would leave a `var` alias into it dangling). ## `tests/ic/timporthidden.nim` is what says all of this still holds. +## +## THE C COMPILER is the largest CPU item of a cold Atlas build and the smallest +## wall lever, which is worth writing down so nobody spends a week on it. gcc is +## 12.2s of CPU against a whole-program build's 10.2s — but `callCCompiler` +## fans out across cores, so the whole `link` stage is 1.65s of the parallel +## build's 9.66s, and the EXCESS over a whole-program build is ~0.4s of wall. +## +## Where the excess is, measured on Atlas (204 IC TUs / 20.85MB against +## non-IC's 139 / 15.47MB): +## +## function definitions 3097 vs 3471 (IC emits FEWER; `merge` dedups, +## and 0 duplicated definitions) +## prototypes 11782 vs 7185 +64% +## typedefs (instances) 5870 vs 4854 +## declaration bytes 8.14MB (39%) vs 4.37MB (28%) +## body bytes 12.71MB vs 11.10MB +## +## So 3.8MB of the 5.4MB excess is per-TU DECLARATIONS — prototypes and +## typedefs each TU needs for what it references. That is intrinsic to having +## 204 translation units instead of 139, and the only real fix is fewer, larger +## TUs, which trades directly against the thing IC exists for: a sandwich edit +## currently rebuilds exactly one `.c` and one `.o`. +## +## The tempting sub-target is a dead end too: 76 of the 204 TUs contain no +## function definition at all and 53 produce object files that define NOTHING, +## but compiling all 53 costs 0.23s of user time. Skipping them is worth ~5% of +## the C compile and nothing measurable in wall. ## * `-d:icBridgeOnly` builds the buffer but generates off the tree, which ## separates the ENCODER's cost from the READER's. Encoding is free — it does ## not show in wall time at all. diff --git a/compiler/icprof.nim b/compiler/icprof.nim index fd19a45124..2410de2cbf 100644 --- a/compiler/icprof.nim +++ b/compiler/icprof.nim @@ -96,6 +96,11 @@ when defined(icBNodeProf): ## Leaf timing. NOT re-entrant, and the phase slots are not disjoint — ## `tTransform` contains body materialization, `tTyp` reaches `tSym`. Read ## them as nested, not additive. + ## + ## Arms the dump like `prof`/`icProfStart` do. It did not, and so a process + ## whose ONLY instrumentation is a `timed` never reported at all: the + ## `merge`, `emit` and `link` stages were silently absent from every profile. + armProf() let t0 = getMonoTime() body profNanos[s] += (getMonoTime() - t0).inNanoseconds