You seem to have quite a bit of misconceptions, which I'll address alongside your question. If anything is still unclear, please feel free to comment on this answer so I can address it
Safer
Yes, Rust is safer.
Safety is one of Rust raison d'être, after all. Specifically, outside of unsafe
blocks, Rust is memory safe and type safe, that is:
- Memory safe: it is not possible to access memory that has not been allocated, or has already been deallocated.
- Type safe: it is not possible to interpret a memory as if it held the value of a given type, while it actually holds the value of another.
Furthermore, a number of operations that would be Undefined Behavior in C or C++ have well defined semantics in Rust instead, such as signed integer overflows.
Memory Safety -- which underpins Type Safety -- is generally further split into two categories:
- Spatial Safety: the inability to access out of bounds.
- Temporal Safety: the inability to access before allocation/after deallocation.
Rust achieves Temporal Safety by compile-time checks. This does mean a bit more code in the compiler1, but there's no run-time footprint -- whether memory or instructions -- so it's "free" at run-time.
Rust achieves Spatial Safety with a combination of library-provided abstractions, and run-time checks. For example, iteration in Rust is achieved by for x in collection
. For an array this boils down to pointer increment -- just like in C or C++, so no run-time overhead -- except that since the user doesn't manipulate the pointers directly, it's safe.
Apart from that, there are various run-time checks: bounds-checks for index access2, null-checks for Option
, etc... those may or may not be optimized, and thus may lead to a slight run-time overhead. The "trick" of Rust, however, is that if performance really matters (as profiled) and the optimizer is not managing to optimize well-enough, it's always possible to try and massage the code (front-loading a bounds-check, for example) or in the worst case to delve down to unsafe
Rust, and thus achieve the required performance target with only localized "Here Be Dragons" code.
1 Rust compile times are on-par with C++, hence quite slower than C, but the safety checks (borrow checks) are an insignificant part of that. Extensive use of generics, traits, and type-inference make the language much more difficult to compile, and the compilation model (full crate at a time) makes parallelization trickier. Still, work is in progress to parallelize the rustc front-end, which should bring compile times down substantially from 2024 onwards.
2 Rust does NOT check indexes at compile-time in general but checks them at run-time instead. A branch itself is typically not a problem at CPU level, if well-predicted. The real impact of bounds-checks is that unless optimized out, they prevent auto-vectorization and a number of other optimizations. This is why front-loading a bounds-check (before the loop) can be so effective performance-wise: it may unlock all those optimizations.
Faster
Idiomatic Rust is likely faster.
First of all, in a "pedal to the metal" situation, all 3 languages can achieve the same performance. Heck, all 3 languages allow embedding assembly, if it comes to that.
So, what we are really asking, is whether idiomatic (not pessimized, not maximally optimized) code may be faster in one language or another, and in those conditions Rust has a certain number of advantages.
The first and foremost advantage of Rust is one of culture. The Rust community is very concerned about performance. This may seem trivial, but it has deep implications. Most notably, Rust code is always compiled from source, and there is no stable ABI (yet). This means that even the standard library provided data-structures can be remodeled extensively as long as their API is left untouched, and leads to Rust having the best-performing HashMap3 of all languages in its standard library:
- It started with Robin-Hood Hashing with Backward Shifting Deletion, which was already faster than
std::unordered_map
.
- Then moved to a completely different hash-map, based on Swiss Table once Abseil was released.
By comparison, the C++ standard library implementations of std::unordered_map
cannot be changed4, nor can their std::regex
implementations...
The second advantage of Rust is trust5: the Rust developer can put their trust in the compiler, and count on it to prevent users from accidentally fiddling with private data or violating soundness:
- In C, it's common to forward-declare structs in header, but never expose their definition so users can't (accidentally) fiddle with them. Unfortunately, the use of such "opaque pointers" then is generally followed by heap-allocating the struct... which is detrimental to performance. In C++ and Rust, that's never a problem, so C++ and Rust programs allocate less.
- In C and C++, it's common to program defensively. Copies are made when the lifetime of the input is unclear, for example, to avoid use-after-free. In Rust, that's never a problem, so Rust developers are more likely to be brash and avoid copies... knowing the compiler has their back.
And finally, the Rust language has a few tricks up its sleeve that may lead to better code out of the box:
- Rust has fine aliasing control from the get go:
- This means no Strict Aliasing rule is necessary, greatly avoiding
char*
(and co) performance pitfalls.
- This means
noalias
(the LLVM equivalent of __restrict
) being used profusely and automatically, as &mut T
is __restrict T*
.
- Rust has niche optimizations. That is, it knows some types do not use all their values, and will "pack" enum discriminants in the unused values whenever possible. As a result
Option<bool>
is 1 byte (like bool
) and Option<NonNull<T>>
(an option non-null pointer to T
) is the same size as *mut T
(a possibly null pointer to T
).
(It's not all roses, though, the defined behavior of signed integer overflows prevent a number of integer-based loop optimizations... though the impact of that is likely low)
3 Bryan Cantrill, who used to be a C kernel hacker at Sun Microsystems, was surprised the first time he naively translated a little C program he had around to Rust to get a feel of the language. Being its first Rust foray, he expected his lack of proficiency in the language would result in slow code... but his Rust program ran faster than his C one, on top of being shorter! He double-checked the output, and after concluding both were correct, did the only thing that made sense: he profiled them. It turns out that in his C program he had hand-rolled a quick hash-map implementation, since dependencies are so annoying in C, while in Rust he had just used the standard one... and his quick C implementation was quite naive. Nowadays, Bryan Cantrill is a Rust kernel hacker ;)
4 And that's on top of inheriting a crippling "memory stability" guarantee from std::map
, in order to be more of a drop-in replacement.
5 You can't spell Trust without Rust.