This comment is confusing two completely separate concepts: zero cost abstractions and run time bounds checks. Rust does provide zero cost abstractions: if you do something explicitly/manually instead of using the abstraction you get the same generated code.
If you want to combine the two concepts, I guess you could go for an example like this:
if index >= vec.len() {
panic!("out of bounds");
}
let value = unsafe { vec.get_unchecked(index) };
which can be rewritten using higher level abstractions as:
let value = vec[index];
The second is more abstract, and that abstraction is zero cost, you do not pay for using the abstraction more than the explicit code above it.
For that specific example it's hard to be expensive but here's a loop from one of my crates that compiles down to efficient code:
pub fn decode_12be(buf: &[u8], width: usize, height: usize) -> Vec<u16> {
decode_threaded(width, height, &(|out: &mut [u16], row| {
let inb = &buf[(row*width*12/8)..];
for (o, i) in out.chunks_exact_mut(2).zip(inb.chunks_exact(3)) {
let g1: u16 = i[0] as u16;
let g2: u16 = i[1] as u16;
let g3: u16 = i[2] as u16;
o[0] = (g1 << 4) | (g2 >> 4);
o[1] = ((g2 & 0x0f) << 8) | g3;
}
}))
}
The "decode_threaded()" call is a function call that passes in a closure with the inner loop to a generic function that is used to multithread a bunch of similar decoders, instead of repeating that code. And the for loop is actually describing what I want (process every 3 bytes into 2 output values) instead of having me manage some iteration variables and have off-by-one bugs. "chunks_exact" and "chunks_exact_mut" are recent additions that allow me to say that I only want to receive exactly 3 bytes and output exactly 2 values, so if the array is improperly sized the extra at the end just gets skipped. This not only matches the intention of this code (I only ever process 2 pixels from 3 bytes and nothing else will work) but also gives the compiler a better way to lift the bounds checks out of the inner loop and make the code significantly faster (by 2x in some decoders).
Now let's see "decode_threaded":
pub fn decode_threaded<F>(width: usize, height: usize, closure: &F) -> Vec<u16>
where F : Fn(&mut [u16], usize)+Sync {
let mut out: Vec<u16> = alloc_image!(width, height);
out.par_chunks_mut(width).enumerate().for_each(|(row, line)| {
closure(line, row);
});
out
}
Besides allocating the image it uses "par_chunks_mut" to make the code threaded. That's a function that's provided by the rayon crate which manages a threadpool and it's scheduling for me. So I'm even using code written by other people, to build something that's apparently deeply nested even though it needs to be fast. And yet after all this indirection and syntax goodies the end result is efficient machine code that matches or is even better than the original C++ code and not the dynamic runtime like behavior of Ruby/Python that you'd expect from such code.
That's what zero cost abstractions are. After memory and data race safety I think rust shines because it gives me a lot of the ergonomics of Ruby with the speed of C.
Right, and this is equally true in Rust and C++ (and any other systems language). Zero-cost abstractions doesn't mean that the compiler is imbued with magical properties.
And there's your wide execution width making it possible to do the checks in parallel with the access. You are using up entries in your branch prediction structure when you have something like that, as well as instruction bandwidth, but I'd doubt the function will run any slower once past decode.
It is. As you say, there's a category of Spectre bugs where the CPU speculatively assumes a bounds check succeeds, then goes on to execute attacker-provided code.
"C++ implementations obey the zero-overhead principle: What you don't use, you don't pay for".
In Rust you pay for e.g. bounds checking or integer overflow handling or optionals.
But I don't understand why everyone's getting so defensive about this, since it's the only way (static verification and proofs aside) to get the desired safety characteristics...
In Rust release builds the integer overflow of checks are disabled. They're not zero cost and there's no magical way to do it, so it's only done in debug builds.
You can disable them in debug builds as well with wrapping integers. So this is again a practice of making the safer option the default, but won't incur runtime costs if it bothers you.
If there is a way to do it at compile time that will usually be used in rust.
> Rust requires a lot of runtime checks, but that's the price one has to pay for memory safety.
It requires a lot of runtime checks on the boundary between rust and C/C++ code. You cannot trust C/C++ code, so you are forced to check everything. C-function returns a enum value? Check that this value is in the enum, before converting it to the rust enum. Check that there are no situations like
But after you've done all that checks you need to check almost nothing, because if you have a enum value in Rust, you know for sure that it is a valid value. And compiler knows that the value is valid and optimizes accordingly. If you have an &str, you know that it is a valid utf8, you need not to check it on each access, it have been checked already. You got a valid pointer from C? Wrap it into NonNull, so neither you, nor compiler would need to check validity of a pointer once more.
It is unclear thing, who needs more checks -- safe code or unsafe code. I'm gravitating to a belief that unsafe code needs much more runtime checks.
If your use iterators instead of indexes, you have no bound-checks because they are optimized away. I've been working full time with rust for a year and I'd say I need indexes less than 10 of the time, most of which are for fixed-size array with constant indexes, for whom the bound checks are also optimized away. So I'd say they aren't really a problem in practice 95% of the time. If you really need to go unsafe for the last 5%, well you're still 95% safer than C++ :)
Even in that case it's not pointless. You can do a lot of testing and fuzzing with checks enabled to get some confidence that you don't have bugs. Then disable the checks for performance. That's just one option. I think it's nice to have options even if I choose not to use them.
Even better is to use iterators and other abstractions that don't require bounds checking at all. Rust has lots of good tools to build efficient code.
And here I thought rust was all about zero cost abstractions.