Rust Performance Engineering

Bounds-Check Elimination

To guarantee memory safety without garbage collection, indexing a Rust slice (slice[i]) automatically checks i < slice.len() at runtime and panics on out-of-bounds access. However, a runtime bounds check inside a tight inner loop has two costs: the comparison/branch instruction itself, and—far more importantly—the potential panic branch prevents LLVM from auto-vectorizing the loop with SIMD instructions!

Bounds-check elimination writes safe Rust code structured so that LLVM's value-range analysis can prove at compile time that every index is in-bounds, allowing the compiler to delete the bounds check completely without needing unsafe { slice.get_unchecked(i) }.

Why a Bounds Check Can Block SIMD Vectorization:
  `for i in 0..n { out[i] = a[i] + b[i]; }`
       |
       +-- If LLVM cannot prove `n <= out.len()`, `n <= a.len()`, and `n <= b.len()` upfront:
           Each iteration might panic midway through a 4-lane SIMD vector!
           ---> LLVM must keep scalar loop with 3 branches per iteration.

       +-- When proven upfront via `.iter()` / `.zip()` / `assert!(n <= slice.len())`:
           Zero panic branches inside loop!
           ---> LLVM emits unrolled 256-bit AVX2 / 128-bit NEON SIMD instructions!

Four Safe Idioms That Guarantee Bounds-Check Elimination

Safe Idiom Why LLVM Eliminates All Bounds Checks
1. Slice Iterators (for &x in slice, .zip()) slice::Iter advances raw pointers from start to end internally; there is no index comparison or panic branch in its .next() implementation!
2. Upfront Slice Sub-Slicing (let s = &slice[..n];) Performs one single bounds check before the loop (slice[..n]). Inside for i in 0..n { s[i] ... }, LLVM knows s.len() == n and deletes all inner checks!
3. Hoisted assert!(idx < slice.len()) Placing a single assert!(a.len() == b.len() && a.len() == out.len()) before the loop informs LLVM's value range tracker of the exact slice lengths.
4. Bitwise Index Masking (table[idx & 0xFF]) When indexing a power-of-two lookup table ([T; 256]), masking idx & 0xFF proves to LLVM that the index is always in 0..=255, eliminating the bounds check!

Demonstrating Pre-Loop Sub-Slicing and Power-of-Two Masking

/// 1. Eliminating inner-loop bounds checks across multiple indexed slices
///    by sub-slicing to a common length `n` ONCE before the loop:
pub fn fused_multiply_add_slices(out: &mut [f32], a: &[f32], b: &[f32], c: &[f32]) {
    let n = out.len().min(a.len()).min(b.len()).min(c.len());

    // Four bounds checks happen ONCE right here outside the loop:
    let out = &mut out[..n];
    let a = &a[..n];
    let b = &b[..n];
    let c = &c[..n];

    // Inside this loop, LLVM knows `out.len() == a.len() == b.len() == c.len() == n`,
    // eliminating 100% of inner bounds checks and auto-vectorizing into SIMD FMA!
    for i in 0..n {
        out[i] = a[i].mul_add(b[i], c[i]);
    }
}

/// 2. Eliminating lookup-table bounds checks with a compile-time power-of-two mask:
pub fn lookup_hex_nibble(table: &[u8; 16], byte: u8) -> (u8, u8) {
    // Because `(x & 0x0F)` is mathematically guaranteed to be `<= 15`,
    // LLVM emits zero bounds-check branches for `table[...]`!
    let high = table[((byte >> 4) & 0x0F) as usize];
    let low = table[(byte & 0x0F) as usize];
    (high, low)
}

fn main() {
    let mut out = [0.0f32; 4];
    let a = [1.0, 2.0, 3.0, 4.0];
    let b = [10.0, 10.0, 10.0, 10.0];
    let c = [0.5, 0.5, 0.5, 0.5];

    fused_multiply_add_slices(&mut out, &a, &b, &c);
    assert_eq!(out, [10.5, 20.5, 30.5, 40.5]);
    println!("Vectorized FMA result: {out:?}");

    let hex_table = *b"0123456789abcdef";
    let (hi, lo) = lookup_hex_nibble(&hex_table, 0xAB);
    println!("0xAB hex chars: '{}{}'", hi as char, lo as char);
}
Vectorized FMA result: [10.5, 20.5, 30.5, 40.5]
0xAB hex chars: 'ab'

Reserve get_unchecked as a Last Resort

Because pre-slicing (&slice[..n]), .chunks_exact(N), and bitwise masking allow LLVM to eliminate bounds checks in 100% safe Rust, only reach for unsafe { slice.get_unchecked(i) } after inspecting assembly (cargo asm) and confirming with a benchmark that LLVM could not prove an invariant across a complex non-linear index calculation (such as a binary heap or hash probe index).