Repository navigation
Description
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.21sThat'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.72sI 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:
- https://rust.godbolt.org/z/abYTx1nWY (diff between
..and..=no-opforloops) - https://rust.godbolt.org/z/v7osffbdP (diff between
fact_incl()andfac_excl())
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
theemathas commentedon Nov 26, 2025
This is a long-standing known issue, unfortunately. See #45222
theemathas commentedon Nov 26, 2025
As a workaround, you can call the .for_each or .fold methods on the range instead.
Sup2point0 commentedon Nov 26, 2025
Ah, unfortunate. Thanks for the link.