fix(codegen): hoist loop-body allocas to the function entry block - #39
Merged
Conversation
`Stmt::Let` emitted `build_alloca` at the builder's current insertion
point, so a `let` binding inside a loop body placed its alloca inside
the loop block (e.g. `while_body`). An LLVM alloca in a non-entry block
re-executes every iteration — a fresh dynamic stack allocation that is
never reclaimed until the function returns — so the stack grows per
iteration. mem2reg/SROA only seed promotion from the entry block, so
loop-body allocas also survive optimization.
This overflowed the 8 MB main-thread stack in Olorin's
`log_level_scan.ea` on >=1 MB inputs (~8 bytes of stack per input byte)
once its SIMD body carried ~50+ u8x16 `let` bindings. At -O0 the kernel
emitted 68 allocas inside `while_body71`; only the 7 params were in the
entry block.
Add `CodeGenerator::entry_block_alloca`, which builds the alloca with a
throwaway builder positioned before the entry block's first instruction
(the caller's insertion point is left untouched). Apply it to `Stmt::Let`
and the struct-literal `struct_tmp`. `Stmt::ForEach` already implemented
this inline ("Alloca in function entry block ... to avoid stack growth at
O0 where mem2reg does not run") and is consolidated onto the helper.
Regression test `tests/loop_alloca_hoisting.rs` asserts every alloca in
the unoptimized frontend IR lives in the entry block. Verified by a -O0 +
`ulimit -s 2048` runtime A/B on `log_level_scan.ea`: pre-fix SIGSEGV on a
4 MB input, post-fix clean exit on 4 MB and 20 MB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
petlukk
added a commit
that referenced
this pull request
Jun 11, 2026
Patch release over v1.15.0 carrying the single codegen fix from #39 (hoist loop-body allocas to the function entry block). Generated -O3 code for existing kernels is unchanged — verified byte-identical asm across the x86_64 and aarch64 benchmark kernels, so bench baselines are unaffected and need no refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stmt::Letemittedbuild_allocaat the builder's current insertion point, so aletbinding inside a loop body placed its alloca inside the loop block (e.g.while_body). An LLVM alloca in a non-entry block re-executes every iteration — a fresh dynamic stack allocation never reclaimed until the function returns — so the stack grows per iteration.mem2reg/SROAonly seed promotion from the entry block, so loop-body allocas also survive optimization.This overflowed the 8 MB main-thread stack in Olorin's
log_level_scan.eaon ≥1 MB inputs (~8 bytes of stack per input byte) once its SIMD body carried ~50+u8x16letbindings. At-O0the kernel emitted 68 allocas insidewhile_body71; only the 7 params lived in the entry block.Fix
Add
CodeGenerator::entry_block_alloca, which builds the alloca with a throwaway builder positioned before the entry block's first instruction (the caller's insertion point is left untouched). Apply it toStmt::Letand the struct-literalstruct_tmp.Stmt::ForEachalready implemented this inline ("Alloca in function entry block ... to avoid stack growth at O0 where mem2reg does not run") — the loop-var path is consolidated onto the new helper so the two can't drift. The helper ispub(crate), so nodocs/public-api.txtchange is needed.Verification
tests/loop_alloca_hoisting.rsasserts every alloca in the unoptimized frontend IR (compile_to_ir, which skips optimization) lives in theentryblock — deterministic and opt-level-independent.log_level_scanallocas are inentry, zero in any loop.-O0,ulimit -s 2048): pre-fix SIGSEGV (exit 139) on a 4 MB input; post-fix exit 0 on 4 MB and 20 MB with correct results.clippyclean, edited filesfmt-clean.Once merged, Olorin can restore the
WARNING/CRITICALkeywords tolog_level_scan.ea(reverted in v2.8.3).🤖 Generated with Claude Code