Skip to content

perf(size): tsc's 463,716-instruction CJS factory is demoted on arm64 and costs 6.4 MB — a flat instruction ceiling cannot separate it from the 100k function that blows up to 10 GiB #10586

Description

@proggeramlug

Summary

DEFAULT_FAST_EMIT_MAX_INSTRS_UNMEASURED (100,000) applies to aarch64, while x86-64 uses a measured 600,000. Real inputs land inside that gap, so the same program is emitted through LLVM's O0 machine pipeline on arm64 and through the optimized pipeline on x86-64.

Measured on typescript@5.9.3's lib/_tsc.js: 86.4 MB -> 80.0 MB (-6.4 MB, -7.4%) purely from raising the arm64 ceiling to x86-64's value.

The code says this is awaiting measurement, and this issue supplies one data point:

/// aarch64/arm64 — and every other target nobody has measured —
/// therefore keep 100k until someone measures them the way `x86_64` was
/// measured above, at which point `default_fast_emit_max_instrs` grows a
/// match arm and this comment grows a row.
const DEFAULT_FAST_EMIT_MAX_INSTRS_X86_64: usize = 600_000;
const DEFAULT_FAST_EMIT_MAX_INSTRS_UNMEASURED: usize = 100_000;

(crates/perry-codegen/src/inprocess.rs:464-483)

The measurement

Perry 0.5.1579, macOS arm64 (M-series, 64 GB), same source, same compiler, only PERRY_LL_FAST_EMIT_MAX_INSTRS differs. Build output is deterministic, so the size column is exact, not sampled.

budget binary codegen time O0 demotion
100,000 (arm64 default) 86.4 MB 6.9 min yes
600,000 (x86-64 value) 80.0 MB 10.7 min no
delta -6.4 MB (-7.4%) +3.8 min (+55%)

At 100,000 the build reports:

perry: `perry_closure_node_modules_typescript_lib__tsc_js__1` has 463716 instructions
after IR optimization, above the optimized machine-pipeline budget 100000; ...
emitting this unit — all 1 of its defined functions, not only this one —
through LLVM's O0 machine pipeline

463,716 is comfortably under 600,000, so at the x86-64 ceiling the warning disappears and the function compiles through the real machine pipeline.

Merged unit object also shrinks: 100.1 MiB -> 94.3 MiB.

Runtime effect: none material

I expected the O0 pipeline to be a large runtime cost as well. It is not, and that is worth recording so nobody else assumes it:

budget tsc --noEmit demo.ts
100,000 663.29 s user (11 m 33 s wall, 96% cpu)
600,000 644.09 s user (10 m 52 s wall, 99% cpu)

-2.9%, n=1 per arm. I would not claim that as a real effect without repeated interleaved rounds. This is a binary-size lever, not a performance fix. The ~890x gap against node tsc.js (0.78 s) has some other cause and is not explained by the machine pipeline.

Correction: 100k is NOT an arbitrary placeholder

I filed this with an unfair framing and am correcting it. Reading the full doc comment block: the two observations that set 100k are both arm64, and one of them sits directly against the ceiling:

  • a 100,152-instruction Claude Code 2.1.259 function grew past ~10 GiB RSS in the optimized machine pipeline (6 s through an O0 target machine);
  • a 277k-instruction async state-machine function sat in LiveIntervals / register allocation for 16+ minutes at ~10 GiB (3.5 s at ~550 MiB demoted).

100,000 is deliberately placed just below 100,152. That is a tuned number, not a placeholder. What is unmeasured on arm64 is the systematic corpus sweep that x86-64 got, not the value itself. Raising arm64 to 600k would re-admit both pathologies — including a 10 GiB blowup — so the blunt "match x86-64" reading of this issue is wrong and should not be actioned.

What the issue should actually ask

The real gap is that a single flat instruction count cannot separate these cases:

function instrs optimized pipeline
cc 2.1.259 function 100,152 ~10 GiB RSS
async state machine 277,000 16+ min, ~10 GiB
tsc CJS factory 463,716 fine — 10.7 min, no blowup

tsc's function is 4.6x the size of the one that blows up, and emits without incident. So instruction count is the wrong discriminator; CFG shape (async state machines and their live-range structure) is doing the real work. Either the ceiling needs a shape-aware companion signal, or the arm64 sweep needs to establish which shapes are actually dangerous.

The better fix for this particular input is #10575, not this ceiling. Outlining the CJS body would put every chunk far below even 100k, making the ceiling moot here and avoiding the 10 GiB risk entirely. I would treat this issue as blocked on #10575 rather than as a ceiling change.

On the mime citation

A probe of standalone mime@4.1.0 types/other.ts measured its shape constructor at 1,370 instructions, which looks at first glance like it contradicts the comment's "522,756-instruction mime types/other.ts constructor". It does not: the comment's figure is from that module compiled inside the OpenCode corpus on x86-64, which is a different compilation context from the standalone file. Recording it so nobody else re-derives the same false alarm.

Why this is a decision, not an obvious bump

The 55% codegen-time increase is real and is presumably why the conservative default exists. The existing comment also notes two arm64 pathologies inside the 600k band (one spending 16+ minutes in register allocation at ~10 GiB), and that "every CI runner and developer build here is macOS arm64" — so raising the ceiling blindly trades a measured size win for an unmeasured compile blowup.

What this issue asks for is that the arm64 ceiling become a measured number rather than the "nobody has measured this" placeholder, since arm64 is the arch everyone here actually builds on. The 100k-600k band demonstrably contains real inputs.

A narrower alternative that sidesteps the tradeoff: fix #10575 so the oversized function stops existing. Outlining the CJS body would put every resulting chunk far below even the 100k ceiling, making this ceiling moot for this class of input.

Reproduction

npm install typescript@5.9.3
echo 'import "typescript/lib/_tsc.js";' > tsc-entry.ts
# package.json: {"perry":{"compilePackages":["typescript"],"allow":{"compilePackages":["typescript"]}}}

perry compile tsc-entry.ts -o tsc-a                                    # 86.4 MB
PERRY_LL_FAST_EMIT_MAX_INSTRS=600000 perry compile tsc-entry.ts -o tsc-b   # 80.0 MB

Related: #10575 (the oversized function itself), #10574, #8583, #8883.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions