Is bound checking the only runtime cost of Rust?

I was thinking about Rust and how almost all of its safety features are done at compile time. For example: borrow checking, move, lifetimes, etc. The only check that happens at runtime, if I'm correct, is the bound checking for slices/arrays accesses.

Are there more runtime costs? Is there a way to know if the compiler was smart enough to know that bound checking wasn't needed in a loop? Can I help the compiler doing it?

Other than manually inspecting the compiler output there isn't a way to see how “smart” the compiler was w.r.t. bounds checking.

Using iterators is generally a good way to avoid needless bounds checking.

1 Like

Bounds checking is the primary one. As for verifying whether it was compiled out, the only way to do that is to view the resulting instructions. And yes, there are ways to help the compiler out. For example, you might have two vectors that you know are the same size, and then write the following code:

for i in array1.len() {
    println!("{} {}", array1[i], array2[i]);
}

Here the compiler can tell that array1 wont go out-of-bounds, but it probably can't for array2, and will insert a bounds check in every iteration. However, you can help the compiler out like this:

assert_eq!(array1.len(), array2.len());
for i in array1.len() {
    println!("{} {}", array1[i], array2[i]);
}

With this code, you still have the assert in the beginning, but you wont have any bounds checks in the loop, and the bounds checks in the loop are a lot more expensive than the assert.


Of course, the other way is to use iterators. Most iterators contain unsafe code that removes the bounds checks, so using them is guaranteed to not have bounds checks.

2 Likes

There are all sorts of different runtime costs associated with various language and library features:

  • Method calls on dyn Trait objects usually compile to indirect ("virtual") calls.
  • Checked arithmetic (which returns None on over- or underflow instead of wrapping around) may require an additional branch instruction.
  • Running destructors may involve implicitly calling potentially expensive computations.
  • The presence of panicking can prevent some optimizations.

These are not a huge issue in most code, however. If you have some code that is not fast enough, you'll need to check using a profiler whether any of these scenarios apply.

1 Like

For what it's worth, bounds checking on slices isn't an overhead imposed by the language. It's a library decision that indexing out of bounds should panic instead of triggering UB.

2 Likes

Can someone tell me how can iterators avoid bound checking? I totally have no idea

They avoid bounds checking with unsafe code, using methods such as get_unchecked.

I don't understand. It's indeed a library decision, but that decision determines whether indexing methods are marked unsafe. In reality, of course, both safe and unsafe alternatives exist.

In other words, since the question is about "safety features", Rust does require run-time bounds checking in safe code, unless the compiler is able to predict the result.

I see it like this:

If I ask you to look in all those boxes over there and find which one has a cat in it, then you have to inspect everyone of those boxes and either find a cat or stop when you run out of boxes. You have to check the number of boxes yourself.

On the other hand, if I ask you to check 10 of those boxes over there, and I know there are 10 or more, then you have to inspect 10 boxes, you never have to look to see if there is a new box or not because it is sure there will be.

Sorry for the strange analogy, but the first is like a loop over some array using C style array indexing. In order to ensure out of bounds access does not happen every iteration has to check if it is still within the length of the array. But when using an iterator the compiler knows what length is available and generates code to do exactly that. No checks required.

There is an “overkill” way to do this: it is possible to cause a compilation error if some code would panic. Since every “invisible” bounds check that doesn't return an explicit success-or-failure value must panic, this also prohibits the kind of bounds checking you're probably interested in detecting. However, it also bans all other uses of panics, such as an .unwrap() you write yourself.

is there an example I can look at?

One way I can think is with a linked list, but you still have to check if the next element is non null, so it's still like a bound check. I can only imagine the interator getting the next element by doing some sotr of checking

Well, you can only avoid the bounds check when it's unnecessary. As an analogy, image if you already know what the length of the linked list is because you stored it elsewhere. In that case, you don't need to perform the null check each time because you know you are not at the end yet.

but I still need to check if I'm still inside the linked list, don't I?

There are also checks for RefCell Borrow, checks that unicode strings are valid and many other checks. These checks are not part of the language exactly, they are checks by library code to ensure that a program behaves in a predictable way. It's possible to disable the checks ( for example you can use unsafe, get_unchecked and get_unchecked_mut to access a Vec without index checking), but mostly it's not worth the bother, as the cost tends to be very small ( my guess is most programs would run less than 1% faster without the checks) and if you make a mistake, the consequences are unpredictable, especially if you end up reading or writing some random bit of memory. Sometimes you can help the compiler out, by moving checks out of inner loops, for example.

1 Like

I agree. It's just that people tend to say "rust is slower than C/C++" and I wanted to have better arguments. I also think that disabling the checks are almost never worth. But would be nice to know how to use iterators as suggested here.

Well, imagine the following example:

fn get_nth(list: &LinkedList<T>, idx: usize) -> &T {
    assert!(idx < list.len())
    let mut node = list.first;
    for _ in 0..idx {
        node = node.next; // no null check here!
    }
    node
}

The above code is not using the real LinkedList, but rather a made-up one for illustration purposes. In a situation like the above, you can avoid a null check in each iteration because you already did the bounds check and don't need to do it again.

indeed. However I was thinking about simpler examples. Whenever I access a slice element, I see no way of doing it safely without doing a bound check, unless in cases such this

let s:[u8, 20] = ...
for i in 0..10 {
    let x = s[i]
}

the compiler knows that i will never be more than 10.

However using iterators would result in a similar bound checking mechanism whenever I want to get the nth element of a slice

No, iterators would have less bounds checks in your code snippet. Your snippet performs a bounds check inside the s[i] operation, but the following code does not have any equivalent to that check due to being based on iterators:

let s:[u8, 20] = ...
for x in &s[0..10] {
    ...
}

The idea to avoid the indexing. Here's a simple example, both loops do the same:

fn main()
{
  let mut a = Vec::<u64>::new();
  a.push(1);
  a.push(2);

  for i in 0..2 
  {
    println!( "{}", a[i] );
  }

  for x in &a[0..2]
  {
    println!( "{}", x );
  }
}

There are checks for RefCells. Rc and Arc, well, count. Standard I/O is synchronized. You're very encouraged to check your returns for errors (Option, Result). Boxes, dyn Trait, and function pointers involve indirection. Some of these get optimized away, some don't.

You can remove some guard rails in Rust and gain some optimization, sometimes. Other times attempting to do so hurts performance in addition to safety. In many cases, the considerations are the same in C/C++, other than the starting point: you have to build a lot of your own guard rails in C/C++.

You may enjoy this blog series: Learn Rust the Dangerous Way - Cliffle

1 Like