Skip to content

Notable performance difference between using exclusive .. range and inclusive ..= range in for loop #149338

Closed as duplicate

Description

@Sup2point0

Hi, I'm very new to Rust, apologies if I get anything wrong!

I was exploring Rust's performance by trying different implementations of the factorial function.

I found that 2 near-identical implementations gave wildly different results. The first (fact_excl()) uses an exclusive .. range; the second (fact_incl()) uses an inclusive ..= range.

..= leads to significantly worse performance, perhaps indicative of a missed compiler optimisation? Here are the implementations and benchmark code:

#![feature(test)]
extern crate test;


/// exclusive range
fn fact_excl(n: u128) -> u128 {
    let mut acc = 1;
    for i in 2..(n+1) {
        acc *= i;
    }
    acc
}

/// inclusive range
fn fact_incl(n: u128) -> u128 {
    let mut acc = 1;
    for i in 2..=n {
        acc *= i;
    }
    acc
}

/// baseline for benchmarks to compare against
fn fact_naive(n: u128) -> u128 {
    if n == 0 { 1 } else { n * fact_naive(n - 1) }
}

/// incorrect implementation of `fact_excl()` without increment
fn fact_wrong(n: u128) -> u128 {
    let mut acc = 1;
    for i in 2..n {
        acc *= i;
    }
    acc
}


#[cfg(test)]
mod bench {
    use crate::*;

    const N: u128 = 1_000;

    #[test]
    fn test_fact() {
        const T: u128 = 12;
        const E: u128 = 479001600;
        assert_eq!(fact_excl(T), E);
        assert_eq!(fact_incl(T), E);
        assert_eq!(fact_naive(T), E);
        assert_ne!(fact_wrong(T), E);
    }

    #[bench]
    fn bench_fact_excl(b: &mut test::Bencher) { b.iter(|| fact_excl(N)) }

    #[bench]
    fn bench_fact_incl(b: &mut test::Bencher) { b.iter(|| fact_incl(N)) }

    #[bench]
    fn bench_fact_naive(b: &mut test::Bencher) { b.iter(|| fact_naive(N)) }

    #[bench]
    fn bench_fact_wrong(b: &mut test::Bencher) { b.iter(|| fact_wrong(N)) }
}

I ran many benchmarks for different magnitudes of const N. Here are the results for N = 100 with overflow-checks = false in Cargo.toml:

# (filepaths anonymised)
PS C:\~\rust-perf> cargo bench
   Compiling rust-perf v0.1.0 (C:\~\rust-perf)
    Finished `bench` profile [optimized] target(s) in 1.46s
     Running unittests src\main.rs (target\release\deps\rust_perf-822fa27acf789e10.exe)

running 5 tests
test bench::test_fact ... ignored
test bench::bench_fact_excl  ... bench:           0.60 ns/iter (+/- 0.11)
test bench::bench_fact_incl  ... bench:         319.34 ns/iter (+/- 222.16)
test bench::bench_fact_naive ... bench:           0.60 ns/iter (+/- 0.17)
test bench::bench_fact_wrong ... bench:           0.42 ns/iter (+/- 0.38)

test result: ok. 0 passed; 0 failed; 1 ignored; 4 measured; 0 filtered out; finished in 4.21s

That's a HUGE difference when using ..= inclusive range. Completely different orders of magnitude, what's going on?

Again, with overflow-checks = true:

test bench::bench_fact_excl  ... bench:           0.66 ns/iter (+/- 0.36)
test bench::bench_fact_incl  ... bench:         188.84 ns/iter (+/- 16.95)
test bench::bench_fact_naive ... bench:           0.36 ns/iter (+/- 0.04)
test bench::bench_fact_wrong ... bench:           0.35 ns/iter (+/- 0.39)

test result: ok. 0 passed; 0 failed; 1 ignored; 4 measured; 0 filtered out; finished in 9.72s

I would try for larger N, but my laptop freaks out and gives +/- error margins larger than the mean(?). However, @HexoKnight tested on their Linux machine with N = 100_000 and got the same notable performance difference:

test bench_fact       ... bench:      23,409.27 ns/iter (+/- 1,253.73)
test bench_fact_wrong ... bench:      23,741.88 ns/iter (+/- 1,043.04)
test bench_fact_incl  ... bench:      76,533.18 ns/iter (+/- 1,965.47)
test bench_fact_excl  ... bench:      23,460.79 ns/iter (+/- 518.60)

I'm not familiar at all yet with this low-level performance-critical stuff, but HexoKnight pointed out differences in the compiled assembly of a for _ in x..y loop VS a for _ in x..=y loop. They're thinking the ..= is disabling SIMD optimisation. See:

Not sure if this is a bug or missed optimisation, thought I'd report it. I would also very much be interested in understanding why what's happening is happening, if anyone has the answers!

I would think .. and ..= use the same underlying machinery? But... is it because x..=y == x..(y+1) isn't guaranteed due to overflow?

I did try searching the rust-lang/rust issues for anyone who's already brought this up, but couldn't find anything remotely related...

In case it's relevant, here were my machine details:

  • rustc --version == rustc 1.93.0-nightly (c871d09d1 2025-11-24)
  • Windows 11
  • Intel Core Ultra 7 155U @2.10GHz
  • 16 GB RAM

Thanks for your time :D

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 26, 2025
  2. theemathas commented on Nov 26, 2025

    @theemathas
    Contributor

    This is a long-standing known issue, unfortunately. See #45222

  3. theemathas commented on Nov 26, 2025

    @theemathas
    Contributor

    As a workaround, you can call the .for_each or .fold methods on the range instead.

  4. Sup2point0 commented on Nov 26, 2025

    @Sup2point0
    Author

    Ah, unfortunate. Thanks for the link.

  5. added
    I-slowIssue: Problems and improvements with respect to performance of generated code.
    on Nov 26, 2025
  6. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-slowIssue: Problems and improvements with respect to performance of generated code.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions