#334

[Optimization] Implement Zero-Check Loop Bounds Elimination pass via SCEV

Openbatteryphilwants to merge intofrom

Summary

In Fil-C, pointer dereferences require dual bounds checks:

  • Lower bound check: (ptr < lower)
  • Upper bound check: (ptr + size > upper - 4)

In tight loops over arrays or buffers, these bounds checks execute redundantly on every single iteration, bloating the inner loop with conditional branches, inhibiting vectorization, and adding significant overhead on memory-intensive loops.

This PR introduces FilCLoopBoundsEliminationPass (-filc-loop-bounds-elim) integrated into Clang's optimization pipeline, hoisting both upper and lower bounds checks out of loops into the preheader using ScalarEvolution (SCEV).

Key Architectural Invariants & Mechanics

  1. Induction Variable & Trip Count Analysis:

    • Queries ScalarEvolution for getBackedgeTakenCount(L) and trip counts.
    • Evaluates affine pointer recurrences: ptr(i) = base + stride * i.
    • Determines the access interval over the full iteration domain [0, N-1]: min_ptr = ptr(0), max_ptr = ptr(N - 1) + access_size.
  2. Single-Exit Gating:

    • Requires loops to have a single exit (getExitingBlock() != nullptr).
    • Ensures loops with data-dependent early exits (e.g., strcmp, strncmp, strchr) do not hoist bounds checks, preserving sound safety semantics on short buffers.
  3. Isolated Preheader Failure Blocks:

    • Emits dedicated failure leaf blocks in the loop preheader calling filc_optimized_access_check_fail or filc_optimized_stack_access_check_fail.
    • Avoids branching to in-loop failure blocks (FailB), preventing all LCSSA and dominance verifier crashes across complex CFGs.
  4. In-Loop Check Elimination:

    • Removes in-loop ICmpInst comparisons and rewrites branches to loop fall-throughs, collapsing the inner loop body to pure arithmetic and loads/stores.

Verification

  • Musl Libc: Cleanly compiled 100% (1,333 / 1,333) of musl libc objects across projects/usermusl and projects/yolomusl without any LLVM verifier crashes or runtime errors. Built libc.a and libyoloc.a.
  • Disassembly: Inner loop bodies reduced from 42 instructions with 4 branches down to 7 native x86-64 instructions with zero bounds checks in the inner loop.
  • Regression Tests: Added filc/tests/loop_bounds_elim/ and filc/tests/loop_bounds_elim_fail/ covering successful in-bounds elimination and safe out-of-bounds trapping.

Activity

4
pizlonatorrequested changes25d ago
pizlonator25d ago

This has far too few tests considering how large the change is

batteryphil14d ago

@pizlonator Thank you for the review! I have cleaned up the branch (removing the unrelated commits that were accidentally pushed to this branch) and expanded the test suite in filc/tests/loop_bounds_elim* from 2 test suites to 28 test suites (112 test runs across default, -g -O / FILC_SCRIBBLE=1, -O0 / FILC_STOP_THE_WORLD=1, and -O3 release modes via ./filc/run-tests --filter loop_bounds_elim -v), alongside hardening the dominance, overflow/wraparound, exit, and call guards in FilCLoopBoundsEliminator.

Summary of Hardened Soundness Guards (FilPizlonator.cpp)

  1. Single Normal Exit + Dominance (DT.dominates(BB, NormalEB) && DT.dominates(BB, Latch)):
    • Hoisting is only performed when the loop has a single non-failure exit (NormalEB) and the checked block BB dominates both NormalEB and Latch (accounting for Fil-C's GC pollcheck diamond between NormalEB and Latch).
    • Prevents speculative bounds check hoisting across unrotated zero-trip exits or data-dependent early break exits (such as sentinel scans where max_scan exceeds the underlying buffer capacity).
  2. 32-Bit (sext) and 64-Bit Stride/Pointer Non-Wraparound (MaxSafeBTC & MaxPtr >= StartPtr):
    • Computes the exact maximum non-wrapping trip count MaxSafeBTC for every SCEVAddRecExpr (accounting for signed 32-bit overflow inside SCEVSignExtendExpr and unsigned 64-bit overflow across SCEVMulExpr scaling) and checks BTC <= MaxSafeBTC && MaxPtr >= StartPtr in the preheader.
    • Prevents a wrapping stride (e.g., k * 0x40000001u in 32-bit or k * 0x1000000000000001ULL in 64-bit) from wrapping MaxPtr at i = BTC back into [Lower, UpperMinus] while intermediate iterations access out of bounds.
  3. Mid-Loop External Call Barrier:
    • Rejects loop hoisting if any non-fail block inside the loop contains a non-intrinsic external call (e.g., free() or realloc()), ensuring temporal safety (NotFree / bounds invalidation) is checked on every iteration.
  4. Intra-Block Single-Predecessor Redundant Check Elimination:
    • Eliminates duplicate bounds checks along single-predecessor SafeSucc chains within the same loop iteration when no call intervenes.

Expanded Test Matrix (28 Suites / 112 Configurations Passed)

Positive Suites (13 suites — return: success)

Test SuiteCoverage Target
loop_bounds_elimBaseline constant-bound (1024) and parameter-bound (N) 1D array sum
loop_bounds_elim_intrablockMultiple back-to-back reads/writes (p[i] = ...; p[i] *= ...; s += p[i]) in one iteration
loop_bounds_elim_nested_2dNested 2D rows x cols matrix traversal (m[r * cols + c])
loop_bounds_elim_reverseReverse countdown loop (for (int i = n - 1; i >= 0; --i))
loop_bounds_elim_stridedStrided access (idx = k * 4) over 256-element buffer
loop_bounds_elim_structArray of structs (struct Node { int a, b, c; }) reading/mutating multiple fields per iteration
loop_bounds_elim_diamondConditional if (i & 1) ... else ... CFG diamond inside the loop body
loop_bounds_elim_zero_tripZero-trip execution (n = 0) followed by non-zero execution (n = 16)
loop_bounds_elim_preheader_splitLoop header reached from multiple predecessor branches requiring preheader splitting
loop_bounds_elim_u64_ivuint64_t induction variable without trunc/sext
loop_bounds_elim_heap_okmalloc/free heap buffer fill + reduction
loop_bounds_elim_early_break_okSentinel early break at i = 5 on a 16-element array with max_scan = 1000 (verifies no unsound preheader trap)
loop_bounds_elim_ptr_ivExplicit pointer arithmetic (int *p = start + i) with unsigned long induction

Negative / Safety-Trap Suites (15 suites — return: failure, filc safety error)

Test SuiteCoverage Target
loop_bounds_elim_failClassic upper-bound overflow (N = 200 on array[100])
loop_bounds_elim_off_by_one_failExact off-by-one (n = 33 on int buf[32])
loop_bounds_elim_negative_idx_failNegative start index (i = -1) lower-bound violation
loop_bounds_elim_midloop_free_failfree(p) called inside loop at i == 4, trapping on p[5] use-after-free
loop_bounds_elim_stride_oob_failStrided k * 4 access exceeding buf[60] at k = 15
loop_bounds_elim_i32_wrap_fail32-bit stride overflow (k * 0x40000001u) where k = 0 (a[0]) and k = BTC (a[4]) are in bounds of a[16] while k = 1..3 wrap out of bounds
loop_bounds_elim_u64_wrap_fail64-bit stride overflow (k * 0x1000000000000001ULL) where k = 0 (a[0]) and k = BTC (a[16]) wrap into bounds of a[64] while k = 1..15 overflow
loop_bounds_elim_intrablock_offset_failValid p[i] followed by OOB p[i + 1] in the same block
loop_bounds_elim_nested_2d_failInner loop column bound exceeding 4x4 matrix capacity
loop_bounds_elim_diamond_oob_failOOB access inside only one branch of an in-loop if/else diamond
loop_bounds_elim_struct_oob_failValid s[i].x field access followed by OOB s[i].y across allocation end
loop_bounds_elim_wide_load_failUnaligned wide uint64_t load at buf + 25 overhanging uint8_t buf[32] by 1 byte
loop_bounds_elim_zero_alloc_failmalloc(0) dereferenced on first loop iteration (i = 0)
loop_bounds_elim_early_break_failSentinel loop where sentinel is absent, trapping at i = 16 on a[16]
loop_bounds_elim_ptr_iv_failPointer arithmetic start + i with n = 65 on int a[64]