Is bound checking the only runtime cost of Rust?

It's hard to say exactly, since the question is nuanced even for bounds checks. Is bounds checking a cost "of Rust" when, say, C++'s vector::at has the same check as Rust's Vec::index?

The best way is to assert! your preconditions so that the compiler has more information.

Here's my standard example: https://rust.godbolt.org/z/bh7h4Gn1f

This does three bounds checks

pub fn demo1(x: &[i32]) -> i32 {
    x[0] + x[1] + x[2]
}

Whereas these two examples each only have one bounds check:

pub fn demo2(x: &[i32]) -> i32 {
    assert!(x.len() >= 3);
    x[0] + x[1] + x[2]
}
pub fn demo3(x: &[i32]) -> i32 {
    let x = &x[..3];
    x[0] + x[1] + x[2]
}

Optimizers are really powerful, and what may look like "more work" can actually be faster if it tells them useful things.

(The reason it can't optimize the first one more is that the panic message for indexing includes the out-of-bounds index, so it needs all the checks to show the "correct" index. The others check a condition that encapsulates the individual index checks, so the optimizer knows those will never fail and can remove them.)

Here's another fun example: https://rust.godbolt.org/z/74KqKo659

5 Likes

Reinforcing this,

pub fn sum3(x: &[i32]) -> i32 {
    x[2] + x[1] + x[0]
}

also only has one bounds check, because the second and third index lookups cannot fail.

5 Likes

Only people who have never written any Rust. What better argument than measurement?: Rust vs C gcc - Which programs are fastest? | Computer Language Benchmarks Game

1 Like
x[2] + x[1] + x[0]

I didn't see that one coming… Not that scottmcm's demo2 and demo3 shout it out, but at least they feature a line that makes you wonder.

Where it really matters, is there a way to express such performance expectations/considerations in readable code, other than a comment? An attribute maybe?

I would just write assert!(x.len() >= 2) and use the ascending order. That makes the assumption pretty obvious to both the reader and the optimiser.

2 Likes

There's a couple options for that.

  1. If it's an implementation detail in one place, you make sure you have a criterion benchmark for the performance of the method, so that people are free to change it, but the benchmarks will notice if what they removed was actually important.

  2. You rewrite it in a way that doesn't actually need to do those checks.

For example, the contrived demo could be done like this: https://rust.godbolt.org/z/znWY5Mf5o

pub fn demo4(x: &[i32]) -> i32 {
    if let [a, b, c, ..] = *x {
        a + b + c
    } else {
        panic!("not enough elements")
    }
}

which also only does one bounds check. Often this can take the form of rewriting to iterators instead.

  1. If it's something that needs to be more pervasive, then you find a way to encode the necessary information in the type system somehow so it's known later -- like how once you have a &str you don't need to do UTF-8 checks any more.

For example, arrays have known length so often remove bounds checks easier than slices -- here's my favourite example of that one https://rust.godbolt.org/z/3MPGdE8sr.

For the silly demo, you can rephrase the question to "I'd like a 3-element array from the beginning", because indexing that is clearly fine. So that could be something like https://rust.godbolt.org/z/YqbzafMh6, which is also only one bounds check:

#![feature(slice_as_chunks)]
pub fn demo5(x: &[i32]) -> i32 {
    let (chunks, _remainder) = x.as_chunks::<3>();
    let first_chunk = chunks[0];
    first_chunk[0] + first_chunk[1] + first_chunk[2] 
}

Edit: oh, there's a newer method that makes that way easier to write https://rust.godbolt.org/z/svdzsf3eM

#![feature(split_array)]
pub fn demo6(x: &[i32]) -> i32 {
    let (first_chunk, _rest) = x.split_array_ref::<3>();
    first_chunk[0] + first_chunk[1] + first_chunk[2] 
}

:+1: to this. The assertion -- which can have a nice meaningful message on failure, not just a context-less "uh, bad index" -- is helpful to both the computers and the humans.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.