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.
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'slib/_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:
(
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_INSTRSdiffers. Build output is deterministic, so the size column is exact, not sampled.At 100,000 the build reports:
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:
tsc --noEmit demo.ts-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:
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:
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
mimecitationA probe of standalone
mime@4.1.0types/other.tsmeasured its shape constructor at 1,370 instructions, which looks at first glance like it contradicts the comment's "522,756-instructionmimetypes/other.tsconstructor". 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
Related: #10575 (the oversized function itself), #10574, #8583, #8883.