Skip to content

chore(quest): report that a subset compiler would not beat rustc on moq - #615

Merged
kixelated merged 7 commits into
mainfrom
quest/m4/subset-compiler
Nov 7, 2026
Merged

kixelated merged 7 commits into
mainfrom
quest/m4/subset-compiler

Conversation

@kixelated

@kixelated kixelated commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

quest/m4/subset-compiler.md asks for a report: could a compiler for a stricter Rust subset (poll-first libraries with no async fn bodies, thin generics, few proc macros) build moq faster than rustc does, now that moq follows those rules? Its prerequisites have landed: moq's poll-first epic closed in moq-dev/moq#7607, and the thin-generics rule is recorded in moq's quest/m0/ci/README.md.

A maintainer's hint added a hypothesis to test: much of rustc's slowness is LLVM. So this report splits rustc's time between frontend and LLVM, measures how much LLVM IR each crate emits and why, and asks how much of a subset compiler's win Cranelift, thinner generics, or less IR could take instead.

Approach

Everything builds -p moq-relay at moq main (f2f9bc935e) on the bench's pinned nightly (1a2c4797e, 2026-10-21), as moq configures it: dev profile, debug = "line-tables-only", incremental on unless a row says otherwise. Each build has its own CARGO_HOME and target directory under /var/tmp, so ~/.cargo/config.toml doesn't apply.

  • Instructions: perf stat -e instructions:u over the whole cargo run. The 32-core host was at load 30 to 45, so I don't report wall time from it. hyperfine reported no instruction counts on these long runs, so perf stat replaced it.
    • Two clean rounds and three edit rounds per configuration, interleaved and rotated.
    • An A/A control moved instructions by 0.003% clean and 0.03% on the edit.
  • Stages: -Z self-profile with LLVM events on a clean build and two moq-net edits, bucketed by query label. Read the shares, not the absolute times.
    • LLVM_passes, codegen_crate, and finish_ongoing_codegen are left out, since their self-time is the coordinator waiting on workers.
    • A second, non-incremental profile without debuginfo stands in for builds through a compilation cache such as mbx, which needs incremental off.
  • IR: a clean build with --emit=llvm-ir -C codegen-units=256.
    • The explicit CGU count matters: without it, --emit=llvm-ir makes rustc use one CGU per crate.
    • Every define is attributed from its v0 symbol to the crate that defines the item. v0 appends the instantiating crate to generic instances and to O0 copies of #[inline] items, and generic arguments in the path tell those two apart.
    • Size is the instruction count, which O0 codegen time roughly follows.
  • ra-jit: the bench harness at moq main, 3 runs each of edit-hub, edit-sync, and edit-async.
  • Mono: the existing -Z dump-mono-stats dump, for MIR by defining crate.
  • CI: clean-build critical paths and the older-pin Cranelift rows come from the 2026-11-06 nightly bench (4 vCPU, moq f36c848).

Where rustc's time goes

Shares of rustc self-time, without coordinator waits:

stage clean build moq-net edit, incremental moq-net edit, non-incremental
expand & resolve 12% 26% 8%
analysis 47% 26% 46%
metadata + incremental 5% 28% 4%
mono 3% 10% 5%
building LLVM IR 7% 0% 8%
LLVM 21% 0.1% 28%
link 5% 10% 1%
CGUs codegened 3591 1 124
  • The hint holds for clean and non-incremental builds, not for the edit moq's developers make.
    • An incremental moq-net edit codegens one CGU. Its cost is the frontend and incremental bookkeeping, rerun in all 10 crates that depend on moq-net.
    • A clean build spends 28% in IR building plus LLVM. A non-incremental edit, which is what a compilation cache forces, spends 36%, and 41% with mono.
    • cargo check costs 57% of a clean build's instructions (440 G of 767 G), which cross-checks the split.
  • At O0, LLVM's time is machine-code emission, not optimization.
    • LLVM_module_codegen_emit_obj is 73 s of LLVM's 92 s on the clean build, and the optimization pipeline is under 1 s.
    • That is exactly the part Cranelift and TPDE replace.
  • Cargo's "codegen" section is not LLVM. On the edit it reads 11.9 of 33.6 unit-seconds, but that is mono collection, incremental loading and saving, and metadata. LLVM's own events sum to 0.02 s.

Levers inside rustc

Retired instructions, in billions (change against LLVM):

scenario LLVM -C debuginfo=0 Cranelift
clean build 767.1 715.0 (-6.8%) 641.4 (-16.4%)
clean build, non-incremental 730.4 680.1 (-6.9%) 611.4 (-16.3%)
moq-net edit 39.9 39.0 (-2.3%) 40.4 (+1.1%)
moq-net edit, non-incremental 224.6 197.5 (-12.1%) 167.6 (-25.4%)
  • Cranelift removes roughly 60% of what IR building and LLVM cost on a clean build. On CI's 4 vCPUs at the older pin, it cut the clean build's wall time by 8.6% and its CPU by 14.2%.
    • It does nothing for the incremental edit, where there is no codegen left to replace.
    • It is the biggest single lever for cache-backed, non-incremental builds: -25%.
    • The Cranelift runs keep rind on LLVM, because its global_asm! sym operands don't build with the Cranelift that ships with this nightly.
    • moq's tests still can't use Cranelift (quest/m3/cranelift-unwind.md).
  • TPDE targets the same emission step and claims 10-20x over LLVM -O0. At that speed it would remove most of LLVM's 21% of a clean build, but not the 7% spent building IR.
  • Debuginfo: line tables are 31% of the IR's bytes but add no instructions. Dropping them saves 7% clean, 12% on a non-incremental edit, and 2% on the edit.
  • Incremental compilation beats every backend lever on edits: 5.6x fewer instructions than the non-incremental edit.

What the IR is made of

Instructions in unoptimized IR, by the crate that defines each function (9.35 M instructions, 1.65 GB):

origin all crates moq's crates moq-net
the crate's own non-generic code 32% 28% 35%
its own generics 8% 7.5% 8.7%
std's generics 36% 38% 46%
dependencies' generics 17.5% 23% 9%
#[inline] copies from other crates 7% 3% 1%
  • Generic instances are 61% of all IR, and 69% of what moq's crates emit. moq's crates are themselves 45% of the build's IR.
  • moq's own generics are what the thin-generics rule targets, and little is left of them.
    • They are 8% of the IR. Removing them entirely would save about 2.5% of a clean build: 8% of the 31% spent in mono, IR building, and LLVM. Edits gain nothing, since incremental reuses unchanged instances.
    • moq already measured sharing BTreeMap key types and found no CPU change beyond noise (moq's .github/btree-build-audit.json).
  • The mass is std's generics at moq's types.
    • BTreeMap's internals alone are 25% of moq-net's monomorphized MIR, across 38 key/value pairs by moq's own count.
    • 36% of moq-net's std-generic instructions take a closure as a generic argument.
    • Only polymorphic codegen (an upstream experiment) or a type-erased collection could shrink this. It is worth at most about 11% of a clean build and nothing on edits.
  • The CGU count doesn't change IR volume at O0. 256 CGUs and 1 CGU give the same 9.35 M instructions, with no function defined twice: #[inline] items are not copied into every CGU at O0, and share-generics reuses upstream instances.

What a subset compiler could win

The subset: moq's own build rules.

  • Library code is poll-first: no async fn bodies beyond thin wrappers over poll_* functions.
  • Generic entry points delegate to non-generic bodies.
  • Proc macros are few.

The candidates are moq's 13 workspace crates and the binary. Every other crate in the build is third-party and stays on rustc.

Linking.

  • It can't read or write rustc's unstable metadata, so it must own whole crates and cross into rustc-built crates through a C ABI.
  • That boundary can't carry what moq's crates actually compile:
    • 61% of their IR is std's and dependencies' generic code, instantiated at moq's types, closures included.
    • A rustc-built shim can't pre-instantiate Iterator::map over a closure defined in a crate that rustc never sees.
  • So the subset compiler must also compile std's and the dependencies' generic bodies, in the full language std is written in (unstable features, intrinsics, specialization). At that point it is not a subset compiler.

Clean build.

  • moq's crates are 20% of rustc's work here, 61% of it frontend.
  • Even a free subset compiler saves at most 20% of CPU.
  • A Zig-class frontend at 10x rustc's speed, keeping today's backend, saves about 11%.
  • On wall time, moq's crates hold 24% of the clean build's critical path on CI's 4 vCPUs, and about 60% here on 32 cores (where the serial moq-net, moq-tokio, moq-relay chain finishes the build). A 10x frontend would cut about 13% and 33% of wall time.
  • Cranelift alone already gives -16% instructions across all 300 crates, and TPDE aims higher.
  • The parallel frontend targets the same serial chain. I didn't measure it here: on CI's 4 vCPUs the jobs-frontend variant moved a clean build by +1.7%.

moq-net edit.

  • 65% of its critical path is crates rebuilt only because moq-net changed. A per-item incremental compiler could in principle get the edit down to about the link, roughly 10x.

  • ra-jit already gets there without a new frontend. It uses rust-analyzer plus Cranelift and splices the changed functions into the linked program.

    edit at moq main cargo ra-jit
    edit-hub 27.8 CPU-s 0.4 s wall, 0.67 CPU-s (-97.6%), served 3 of 3
    edit-sync 18.5 CPU-s 0.24 s wall, 0.41 CPU-s (-97.8%), served 3 of 3
    edit-async 20.1 CPU-s 0 of 3 served, handed back to cargo
    • Wall times for cargo are meaningless on this busy host. CI's nightly at the older pin measured 8.22 s against 0.14 s for edit-hub.
    • ra-jit's daemon holds about 6 GB.
    • The edit-async fallback at main is ra-jit failing to compile an instance of usage-argv's generic parsed_values_given. It is not inherent to async: under moq's poll-first rule, the logic an edit touches lives in sync poll_* functions, which ra-jit serves.
  • What remains for edits that change an API is relinking without rebuilding (quest/m3/relink.md) and upstream's incremental work, neither of which needs a new frontend.

Verdict: no subset compiler

  • Clean build: its ceiling is moq's own crates, 20% of the work. Backend levers that apply to every crate (Cranelift today, TPDE later) reach about as far without leaving rustc.
  • Edit: its best case is already measured on moq main, by ra-jit hot patching. The remaining gap is ra-jit's bugs, not the language.
  • Linking: it must compile std's generics to link with anything, which rules out a subset.
  • The hint: LLVM is a real 21% of a clean build and 28% of a cache-backed edit, almost all of it O0 machine-code emission. It is 0.1% of the incremental edit. A faster backend is the fix for that part, not a new frontend.

This PR deletes the quest and records the verdict in m4's plan, so the idea isn't revived without new evidence.

Impact

  • Public API: none.
  • Quest tree: deletes quest/m4/subset-compiler.md, adds the closing note to quest/m4/README.md, and drops the subset compiler from the root's m4 summary.

Alternatives

  • Keep the quest open for per-item incrementality. ra-jit already shows the edit win that per-item incrementality would buy, and the upstream incremental redesign is a 2026 goal. Nothing here needs a compiler of our own.
  • Rewrite the quest as a type-erased BTreeMap for moq-net. At most about 2% of a clean build and nothing on edits, and moq's own key-sharing trial found no change, so I didn't propose it.

Choices made without asking

  • Closed the quest on the report's verdict rather than leaving it to the maintainer, as the macro-cost report did (moq-dev/rust2#64). The Goal asks for a decision, and the data gives one.
  • Measured at moq main rather than the bench's corpus pin (f36c848), since the question is about moq after poll-first. The CI rows are labelled as the older pin.
  • Measured instructions with perf stat instead of hyperfine, which returned no counts on these runs.

Follow-ups

  • Bench a TPDE variant once -Zllvm-tpde reaches a nightly the bench can pin (rust-lang/rust#164108). Recommend planning it, as a waiting quest: it targets the 21% of a clean build and 28% of a cache-backed edit measured here.
  • Bump the bench's moq pin past poll-first. Recommend planning it: the pin predates feat!: generate async wrappers behind default-off async features moq#7607, rebuilds 8 crates on the hub edit where main rebuilds 10, and still edits the pre-poll-first async fn body.
  • Report ra-jit's edit-async fallback upstream. Recommend dropping it: ra-jit lives outside the kixelated and moq-dev orgs, and the bench README already tracks its moq bugs.

Models

Size: XL
Implementer: Claude Code, claude-opus-5-5, unknown effort (the killed starting session)
Implementer: Claude Code, claude-opus-5-5, high effort

(Written by Claude Opus 5.5 with high effort)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, xhigh effort
  • Session: 01a1b3ee-eecd-7672-b5d7-4daef54ba76f, round 2
  • Head: 6b42c0e
  • Base: origin/main at 6a36cb3
  • Score: 10/10, The quest closure remains consistent, the revised performance claims match the report, and just check, quest validation, and git diff --check pass with no defects found.
  • Findings: none

@pullfrog

pullfrog Bot commented Nov 6, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

@kixelated kixelated changed the title A subset compiler chore(quest): report that a subset compiler would not beat rustc on moq Nov 7, 2026
@kixelated
kixelated marked this pull request as ready for review November 7, 2026 02:58
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2f3e37a3-108a-42c3-b637-6df65abe65de
Commits

Reviewing files that changed from the base of the PR and between 6a36cb3 and 6b42c0e.

Files selected for processing: 3

  • quest/README.md
  • quest/m4/README.md
  • quest/m4/subset-compiler.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed head 8bbd21449c32ef5538be76c22b369804dc9c27b9 against base 91a905c94899792e4edade5f89dafa84e16c5034.

No actionable code findings in this three-file planning-only change. The subset-compiler quest is removed from m4's Required list, its closing rationale links to this report, and the root summary now names the existing rustc/cargo fork quest. The report addresses the deleted quest's requested subset, clean-build/edit estimates, and interoperability boundary. No compiler implementation, build configuration or public API changes are included.

Direction: recording the negative result and keeping future fork work evidence-gated is consistent with m4's stated purpose. Minor documentation suggestion: qualify the README's 'cuts a clean build by 16%' as retired instructions, and the sub-half-second ra-jit result as the successful body-edit cases. The report distinguishes these from wall time and the edit-async fallback; keeping those qualifications beside the short summary would prevent overgeneralizing the result.

Verification limits: GitHub-only static review of the complete diff, report and surrounding m4 plan/fork quest. I did not run quest context or validation, reproduce the benchmarks, inspect raw measurement artifacts, or independently establish the numerical estimates or claimed CI success. This is a review of the recorded plan, not experimental validation or merge approval.

@kixelated
kixelated added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit f56859c Nov 7, 2026
4 checks passed
@kixelated
kixelated deleted the quest/m4/subset-compiler branch November 7, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant