I’m not a true expert but I’ll take a stab. I’m not saying that Rust is as good as Julia for scientific computing. But it is both fast and safe. Here’s an illustrative example:
fn main() {
let a = [1, 2, 4];
// Sum of the squares of all of the elements of the array
let mut sum_sq = 0;
for x in a.iter() {
sum_sq += x * x;
}
// Another way:
let sum_sq2 = a.iter().fold(0, |acc, x| acc + x * x);
println!("Sum of {:#?} is {}", a, sum_sq);
assert_eq!(sum_sq, sum_sq2);
let message = "Hèllo";
let e = message.chars().nth(1).unwrap(); // message[1] is not allowed.
println!("Second letter of {} is {}", message, e);
}
Run this here.
I think both of these sum-of-squares functions are equally efficient, and just as fast as C. Possibly faster, since in some cases the compiler will automatically use SIMD instructions. Bounds checking does not slow this down. In each trip of the loop over a, it’s testing whether or not the iterator has reached the end, and that’s as fast as testing an index counter. It’s much more common to use iterators than explicit indexing.
Rust often gives you “zero-cost abstraction.” One manifestation is that these iterators and their many methods will compile down to code that you can’t hand-optimize any better. You get macros like println!
which the compiler first expands into code. The print statement here is faster than printf since the format string parsing is done at compile-time.
Safety goes way beyond bounds checking. Variables and references are immutable unless you declare them with mut
. The language allows and even encourages some functional programming paradigms, so you can write pure functions when it makes sense to. References (borrows) are checked by the compiler in a way that eliminates mistakes leading to segfaults and (most) race conditions in multi-threaded code. Built-in types like Option and Result allow for ergonomic error checking and completely eliminate the need for null values.
Another thing about safety and indexing with regard to strings: Why can’t you take message[1]? Because strings are UTF8 and each code-point is one or more bytes. So you can only really iterate. chars() returns an iterator, and nth() returns an Option. unwrap() on that will panic if the Option enum had the None value, which is a lot like bounds checking. Since I know I’ll get Some('è')
and not None
in this case, I just unwrap() instead of properly handling both cases. There are other libraries that let you iterate over grapheme clusters instead of chars.
I’m giving that example because it helped me understand some of the unusual ways that Rust strives for safety throughout its design. You can index a string directly in Julia, but it looks to me like internally Julia converts UTF8 to a 32-bits-per-char format. Rust can’t do that because as a systems language it has to remain as performant as possible. Sure, [n] is O(1) while chars().nth(n) is O(n), but you aren’t forced to copy incoming UTF8 buffers into an extra-wide array.