Multi-phase optimization pipeline: peephole, SCCP, IVSR, symbol interning - #252
Multi-phase optimization pipeline: peephole, SCCP, IVSR, symbol interning#252CrazyTodd-one wants to merge 21 commits into
Conversation
Add -O0/-O1/-O2 optimization tier support with proper pass gating. New IR passes: dead code elimination, IV strength reduction, integer narrowing, and use-def analysis infrastructure. XMM-through-accumulator peephole fold with liveness safety check. Fix doc-test pseudo-code blocks. Include benchmark test programs and harness. CCC now beats GCC -O0 on matmul (222ms vs 249ms) and sieve (141ms vs 151ms). All 497 unit tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New peephole pass: fold_address_through_secondary eliminates movq %rN, %rcx before memory operations that dereference (%rcx), folding the address directly into the load/store. Removes 6+ redundant instructions per pointer-heavy function (e.g. strprocess count_words hot loop). Extended extension elimination: forward scan now skips non-rax-writing instructions to catch patterns like movsbq (%r15),%rax; movq %rax,%r13; movsbq %al,%rax. Also recognizes cltq after movzbl/movzwl as redundant since unsigned byte/word values always have bit 31 = 0. Upgraded IR-level memcpy from pure rep movsb to rep movsq + rep movsb remainder. 8x fewer loop iterations for struct copies >= 8 bytes. All 497 tests pass. CCC beats GCC-O0 on matmul (11.6% faster) and sieve (10% faster), with 8.8% smaller binaries across all benchmarks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The if_convert pass was unconditionally converting all diamond-shaped CFG patterns to branchless Select/cmov sequences, including nested if/else-if chains where well-predicted branches are faster. This added two limits: - MAX_SELECTS=2: reject diamonds producing 3+ selects (15+ cmov instructions) - MAX_TOTAL_COST=12: cap hoisted instructions + selects*5 total speculated cost Applied to both diamond and triangle detection paths. Reduces cmov count in strprocess count_words from 4 to 2, improving runtime ~3% (3.919s -> 3.801s, 10-run mean). No regressions on matmul, sieve, fib, or Lua compilation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eline Systematic migration of heap-allocated String to reference-counted Rc<str> for all identifier/name fields: lexer tokens, AST nodes, preprocessor macros, sema function/type maps, IR instructions (Call.func, GlobalAddr.name), IrModule collections, lowering state, optimization passes, and backend codegen state. Makes .clone() O(1) instead of O(n) for name fields, reducing allocation overhead in the hot macro expansion and inlining paths. 76 files changed. Rc<str> auto-derefs to &str so most read sites need zero changes. FxHashMap/FxHashSet lookups work unchanged via Borrow<str>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the Wegman-Zadeck Sparse Conditional Constant Propagation algorithm. SCCP reasons about control flow and data flow simultaneously: phi nodes only meet incoming values from executable edges, making it strictly more powerful than iterative dataflow constant propagation. Use-chains added to UseDefInfo via a Compressed Sparse Row (CSR) layout: use_offsets[v..v+1] indexes into use_sites[] for O(1) lookup of all consumers of value v. Built in a single additional pass over the IR. Pipeline integration: SCCP runs after constant_fold at -O2+. Six helpers in constant_fold.rs promoted to pub(crate) for reuse. -O3 now runs 5 iterations (vs 3 at -O2) with a 2% diminishing-returns threshold (vs 5%). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds detection of phi(init, GEP(phi, stride)) patterns for pointer-based loop induction, and BinOp::Add(ptr, mul) pointer arithmetic patterns. Tracks pointer uses separately for correct strength reduction of pointer increment loops (e.g., p++ in array traversal). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-O3 now sets opt_level=3 (distinct from -O2's opt_level=2), enabling the 5-iteration/2% threshold behavior added in the SCCP pipeline commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nvert cost model - driver/README.md: document -O0/-O1/-O2/-O3 tier behavior - ir/README.md: update IrModule/IrFunction field types to Rc<str> - passes/README.md: document optimization tier gating, if-convert cost model - ideas/: mark string interning items as partially fixed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy propagation: extract shared collect_jump_targets from store_forwarding into helpers, add callee-saved register preservation across calls, enable multi-propagation per instruction with re-processing. If-convert: lower MAX_SELECTS from 2 to 1 (2-select costs 12+ x86 insns vs ~4-6 for branch). Combined: strprocess gap vs GCC -O0 narrowed from 33% to 26%. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Call argument emission: add operand_to_named_reg() to load operands directly into target registers (rdi, rsi, rdx, etc.) instead of routing through %rax. Eliminates 10+ instructions in strprocess hot loop. Stack push: push directly from callee-saved register when available. Peephole: fuse movq N(%rbp),%rax + cltq into movslq N(%rbp),%rax. Add ProducerMovqMemToRax ExtKind for stack-to-rax loads, handle in fuse_movq_ext_truncation alongside existing register-source fusion. Results: strprocess gap vs GCC -O0 narrowed from 26% to 15%. Sieve now 14% faster than GCC -O0 (was tied). Binary size savings increased from 8.8% to 10%. CCC beats GCC -O0 on 3 of 5 benchmarks. 514/514 tests pass, sqlite3+Lua+zlib compile at -O2 with zero regressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document direct register routing, load+sext fusion, copy propagation enhancements, and if-convert tightening. Update benchmark results: CCC beats GCC -O0 on 3/5 benchmarks, strprocess gap narrowed from 33% to 15%, binary size savings at 10%. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add FxHashMap<String, String> to Lowerer to track interned string contents. When the same string literal appears multiple times, they now share the same .rodata entry instead of getting separate copies. This matches GCC's default -fmerge-constants behavior. Fixes test failures where code compares string literal addresses (e.g., memcmp on pointers to identical strings). Also reduces binary size for programs with repeated string literals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Supports GAS-compatible blank/non-blank argument testing used in Linux kernel assembly macros (e.g., IBRS_ENTER). Handles bare directive form when macro arguments expand to empty strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Types I8-I32, U8-U32, and F32 now use 4-byte stack slots instead of 8-byte, nearly halving stack frame sizes for functions with many 32-bit temporaries. Fixes pcre2 stack overflow in deeply recursive compile_branch (10.3KB → ~5KB frame per call). Uses movl for store/load to 4-byte slots. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On x86-64, 32-bit register operations implicitly zero-extend the upper 32 bits, making many 64-bit operations unnecessarily wide. This pass narrows them: - andq $imm, %reg → andl $imm, %regd (when 0 <= imm <= 0x7FFFFFFF) - movslq %regd, %reg → eliminate (self-extension after 32-bit op) - movslq %regd, %rax → movl %regd, %eaxd (cross-reg after 32-bit op) - testq %reg, %reg → testl %regd, %regd (after known 32-bit producer) The strprocess count_words hot loop now generates: andl $8192, %edi (was andq $8192, %rdi) movl %edi, %eax (was movslq %edi, %rax) testl %eax, %eax (was testq %rax, %rax) matching GCC -O0's instruction selection for this pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend peephole optimizer with depth-limited cross-block liveness analysis that can follow control flow through conditional jumps, unconditional jumps, and fallthrough-only labels. This enables three key improvements: 1. fold_accumulator_routing: Eliminates redundant register-to-register moves through temporary registers (movq %src, %rT; movq %rT, %rN → movq %src, %rN) 2. fold_increment_in_place: Folds 3-instruction increment patterns through temporaries (movq %rN, %rT; addq $imm, %rT; movq %rT, %rN → addq $imm, %rN) 3. fold_address_through_secondary: Extended with cross-block analysis to fold more address computation patterns (movq %reg, %rcx; movsbq (%rcx), %rax → movsbq (%reg), %rax) Also fixes fold_xmm_through_accumulator to skip memory destinations, preventing cross-domain store forwarding stalls (XMM store → GP load from same stack slot). Benchmarks vs GCC -O0: - matmul: 0.203s vs 0.235s (14% faster) - sieve: 0.113s vs 0.134s (16% faster) - strprocess: 1.747s vs 1.425s (23% slower, unchanged) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bug fixes in the core register liveness scanner: 1. Ret handler: %rax (reg=0) is live at function return since it holds the return value per System V ABI. Previously returned true (dead) for all registers at ret, which could incorrectly eliminate moves that set up the return value. 2. leaq/leal pure overwrite: Removed from the unconditional pure overwrite list. `leaq 8(%rax), %rax` reads %rax in the address computation AND writes to %rax — it's read-modify-write, not a pure overwrite. Now checks if the destination register appears in the source operand before classifying as pure overwrite. No performance impact on existing benchmarks — these fixes protect against incorrect optimizations that would surface with future passes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Six new optimization passes for the x86 peephole optimizer: 1. fuse_and_test_branch: Fuse AND+test+jCC sequences. When `andl $IMM, %reg` is followed by `testl %reg, %reg; jCC`, the test is redundant (AND already sets flags). When the AND result is dead, convert to non-destructive `testl $IMM, %orig_reg`, tracing back through mov chains. 2. fold_address_through_secondary (generalized): Extended from %rcx-only to any GP register. Folds `movq %rA, %rT; <mem_op> (%rT)` → `<mem_op> (%rA)` when %rT is dead. 3. fold_commutative_through_temp: Folds `movq %rA, %rT; movq %rB, %rA; addq %rT, %rA` → `addq %rB, %rA` for commutative binary operations (add, or, xor, and, imul) when the temp register is dead. 4. fold_double_to_leaq: Folds `movq %rA, %rB; addq %rA, %rB` → `leaq (%rA, %rA), %rB` when flags are dead. Only in post-global phases to avoid interfering with fold_commutative_through_temp. 5. fold_movq_addimm_to_leaq: Folds `movq %rA, %rB; addq $IMM, %rB` → `leaq IMM(%rA), %rB` when flags are dead. Also handles subq. 6. fold_scaled_address_into_load: Folds address computation chains into x86 scaled addressing modes. `leaq (%rA,%rA), %rT; addq %rT, %rB; <mem_op> (%rB)` → `<mem_op> (%rB, %rA, 2)` (saves 2 instructions). Also handles the simpler `addq %rT, %rB; <mem_op> (%rB)` → `<mem_op> (%rB, %rT)` (saves 1 instruction). Supporting changes: - are_flags_dead_after: Enhanced to skip labels (flag liveness is forward-only and path-independent at join points), expanded scan window from 6 to 24 instructions. - Extended dead reg move elimination (eliminate_dead_reg_moves_ext): Uses call-safe liveness that treats calls as barriers. strprocess hot loop: 41 → 26 instructions (-37%). All 514 unit tests pass, all benchmarks produce correct output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note: This entire body of work — all 19 commits, 109 files, +6,860 / −836 lines — was produced 100% with Claude Code (Claude Opus 4.6). The work was driven by the IMPROVEMENT_REPORT.md roadmap in the repo. That document identified 20+ improvement opportunities in the original codebase — unfinished agent tasks, missing optimization tiers, no benchmarks, broken Hello World, heap-allocated strings everywhere, and no way to measure performance against GCC. Every item in this PR traces back to that roadmap:
Results from the roadmap's methodology (measure → understand → implement → verify → benchmark):
Claude wrote the compiler. Claude Code improved it. The roadmap was the bridge. |
Convert remaining String fields to Rc<str> across the preprocessor and semantic analyzer for O(1) clone during macro expansion: - MacroDef.params: Vec<String> → Vec<Rc<str>> - MacroDef.body: String → Rc<str> - macro_save_stack, include_guard_macros: String keys → Rc<str> - weak_pragmas, redefine_extname_pragmas: String → Rc<str> - defined_structs: FxHashSet<String> → FxHashSet<Rc<str>> All 520 tests pass, all test programs produce identical output. Co-Authored-By: Claude Opus 4.6
Three new peephole optimization passes: 1. Dead argument register move elimination: removes moves to argument registers (%rdi, %rsi, etc.) before calls to zero-argument functions 2. Extended local pattern matching for redundant operations 3. Loop trampoline optimization for indirect branch simplification Co-Authored-By: Claude Opus 4.6
|
Tested it against a couple of C testsuites (from my rcc): PR #252 adds significant regressions against the rcc test-suite on Linux x86-64: Against ccc baseline:
|
All code in this PR was written 100% by Claude Opus 4.6.
Summary
19 commits implementing a comprehensive optimization and performance improvement pipeline across the compiler backend and frontend:
Backend optimizations (Phases 2–10):
-fmerge-constants).ifnb/.ifbconditional assembly directives in the x86 assembler-O2/-O3optimization tiers in CLI flag parsingFrontend performance (symbol interning):
Rc<str>conversion across IR names, AST identifiers, preprocessor (MacroDef.params,MacroDef.body), and semantic analyzer (defined_structs)macro_save_stack,include_guard_macros, pragma fields) converted toRc<str>keys/valuesBenchmarks vs original upstream
Diff stats
109 files changed, +6,860 / −836 lines
Test plan
cargo build— compiles cleanly (only pre-existingdead_codewarning)cargo test— all 520 tests pass🤖 CrazyTodd + Claude Opus 4.6