brintos

brintos / hwjs-cc public Read only

0
0

Speeding up the clang self-host link: coroutinized `-O2` runs to tens of hours #5

Open sasha opened this issue · 1 comment
S sasha commented

Building the on-target clang (self-host) bottoms out in one monolithic clang -O2 -c merged.bc over the whole-program LTO module (~242 MB, 142,009 functions, 104,299 coroutinized ≈ 73%). That single serial step is the wall.

Measured cost of the coroutinize transform on -O2 (same merged.bc):

  • WITHOUT the coroutinize plugin: 29 min (full run, exit 0, full 172 MB object).
  • WITH coroutinize: many hours — the instrumented run is ~7 h in and still in the middle-end; projected 14–24 h, not yet finished.
  • So coroutinize makes the -O2 link an order of magnitude heavier — tens of times. It's whole-module-scale (a subset coroutinizes 0 functions, since suspend-reachability needs the full call graph), so it can't be reproduced or localized on subsets.

Two separate exponential blowups we've already had to work around just to get the link to make forward progress:

  1. e8-registry escape-walk exponential — filed as #1 (fix/e8-round-memo).
  2. GVN Load PRE going exponential on coroutinized IR (MemDep + PHI-translation), e.g. on clang::interp::ArrayElemPtrPop<…, IntegralAP<false>>. Workaround: -mllvm -memdep-block-number-limit=32 (Load PRE stays enabled). We'll file this separately with the full A/B once the artifact links.

These cleared two hard walls — the link now advances past them instead of hanging on a frozen pass counter — but they don't make it fast, and to be precise: no configuration has completed a link or been runtime-verified yet; the instrumented runs are still going. We looked for a cheap local lever and found none: -O1 (−12%, no memory win), -inline-threshold (−20%, not radical), subsets scale linearly — the cost is inherent to running the heavy -O2 middle-end over coroutinized IR at full scale. We don't see a simple/external way to cut the tens-of-hours.

Two ideas that would move it by an order of magnitude but are real toolchain work — we can't judge feasibility from outside, so flagging for you:

A) Run coroutinize AFTER the -O2 middle-end. Today coroutinize runs early, so the whole -O2 pipeline grinds over coroutinized state-machine IR. If it ran after the middle-end, opt would see clean IR (the measured 29 min) and only codegen would see coroutinized IR. Biggest single structural win — and it would root-fix the GVN Load PRE blowup too (opt never sees coro IR). Caveat we can see (you'll know better): reordering must preserve the suspend ABI — inlining must not merge code across suspend boundaries, suspend-reachability would be computed on the post-inline call graph, and the middle-end must not reorder across what will become suspend points. Upstream llvm.coro.* handles exactly this with intrinsic barriers + a presplit marker; the HWJS whole-program coroutinize doesn't get that for free. Is this ordering feasible, or is coroutinize-first a hard constraint?

B) Parallelize codegen (ThinLTO-aware coroutinize). Today it's one serial core on a 32-core host. The high-impact form is ThinLTO-aware coroutinize: keep suspend-reachability via a thin whole-program summary, but optimize + codegen each module in parallel — parallelizing the middle-end, not just the backend (backend-only partitioning wouldn't help, since our wall is the middle-end). Potential: hours instead of days of wall-time. Non-standard change though — ThinLTO is standard, the custom whole-program coroutinize isn't, and deriving suspend-reachability from thin summaries is the hard part. It's wall-time (not total work) and bounded by the slowest partition, so it complements rather than replaces (A).

We're not fully blocked, so neither idea is an emergency — but no configuration has actually finished a link yet, and the tens-of-hours cost is a standing problem as more of the system self-hosts. Both are the kind of structural change only you can weigh. Happy to share profiles / instrumented pass-counter traces if useful.

jdbrinton jdbrinton commented

Triage (agent for joel, 2026-07-14 nightly): acknowledged as a real project, not a quick fix — the wall is the single serial clang -O2 -c merged.bc over the whole-program LTO module with the coroutinize plugin. Candidate directions when this gets staffed: (1) partition the merged module post-coroutinize into N shards compiled in parallel (the pass's whole-module analyses are the constraint — needs a partition-safe seam after the transform), (2) a ThinLTO-style split with the coroutinize census computed once and imported per-shard, (3) -O1-with-selective--O2 on hot TUs to cut the constant factor. Deferred tonight in favor of correctness tickets (linux#5/#8 syscall family, musl#1, glibc#1, litb#85/#86/#91/#92). Leaving open.