C++ is faster and safer than Rust (2020)
pvs-studio.com
pvs-studio.com
These are the three things that matter most.
To the point though, while some of the points in the article are well-taken, especially how one can pick-and-choose scenarios where one language or the other has a noticeable implementation issue or design misfeature - the article is still problematic for at least two reasons:
1. Mixing up a comparison of the languages and of the results of specific compilations by a specific compiler.
If C++ semantics allow doing something easier, but Rust semantics do not, e.g. because of avoiding undefined behavior, then regardless of whether a specific compiler makes use of this fact - it is still a benefit, speed-wise. Specifically, in Rust, if I take two non-negative numbers and multiple them using the square() function discussed in the article, then check the result for being negative - with C++, the check is redundant and can be dropped in favor of a true value during compilation; with Rust, it cannot. Now, will some version of LLVM do that? I don't know - but it could (and it should).
2. Misrepresenting how the langue affects the behavior of the compiler back-end.
The article says:
> Rust is based on LLVM, which is the same back end that Clang is based on. Therefore, Rust has inherited "for free" and shares with C++ most of the language-independent code transformations and optimizations.
That's simply not true. That is, LLVM is _capable_ of most of the same transformations and optimizations; but the question of which of them it is _allowed_ to use depends on various kinds of semantic information specific to the programming language.
This is apparent even in trivial examples. Here's one:
https://godbolt.org/z/rMW3vsEvo
where the same function is compiled in Rust:
pub fn check(num: i32) -> bool {
let y = if num < 0 { -num } else { num };
return y >= 0;
}
and in C++: auto check(int32_t num) {
auto y = (num < 0) ? -num : num;
return y >= 0;
}
The compilation results are: example::check:
mov eax, edi
neg eax
cmovl eax, edi
test eax, eax
setns al
ret
and: check(int): # @check(int)
mov al, 1
ret
respectively. How come? Isn't it all LLVM under the hood? It's even the same LLVM, version, 12.0.1 for both languages... Now, I'm not sure why exactly this happens since I'm no Rust expert. But - LLVM is not _allowed_ to apply the same optimizations to the Rust function as to the C++ function, so the results are different.C++ will of course always return true.
> In Rust, an undefined-behavior arithmetic issue like that is literally impossible.
... but that's only true if the safeties are on, specifically overflow checking.
What is scary is, I don't doubt that some people will read your comment and think it could be true!
I had misread your original post. I thought the example was something like "std::cout << 97".
This is unfortunate. I imagine it came about when "cstdint" was introduced. Fixing it would require a break in compatibility with any existing code that "expected" that all char types printed a character, rather than value.
> Spoiler: C++ is not faster or slower – that's not the point, actually. This article continues our good tradition of busting myths about the Rust language shared by some big-name Russian companies.
- has been around stable for over 6 years and was already somewhat popular years before that
- is the first real contender to C in the Linux kernel, with support from its BDFL and a lot of work going on, and well those devs know a bit more of pain points in complex systems that need to go fast.
- is used in of the major Browsers while the most popular one is contemplating starting to use it
- allows refactoring and cleaning up code so nicely, it's just marvellous if one actually worked on big code bases previously to rust, as either one had ten static checkers and three times the amount of CI pipelines as required bolted on, or just risk quite a bit every time something was touched.
- deep and mature integration of tests and docs and doc-test (those are damn nice)
- want me to continue? I got a few more points
surely is a fad ;-)
Look, you do not need to use Rust if you do not want too, but living in denial about that Rust has seen a good rate of adoption and that it also has its uses as valid Language to do stuff nowadays, is just not a good thing to do mentally nor socially.
Quite a few processors have some undefined behavior, some instructions flags are just not specified, and no not just "this is reserved and must be zero", that would be Ok.
The result of the `bsf` or `bsr` instructions on the 0 value
And please, don't look too much at the FPU either or it may do some random stuff and jump at you.
But CPU architectures also have undefined behavior. ARM for example calls these "UNPREDICTABLE" and "CONSTRAINED UNPREDICTABLE". Have a at the Armv8 architecture reference and grep for those. Lot's of ways to write programs in pure assembly with undefined behavior too!
x86, 6502 and Z80 etc all have undefined / unpredictable behavior too.
EDIT: Thanks to IcePic's answer, I found it here: https://web.archive.org/web/20210405071521/http://visual6502...
And for the last part of your sentence, it depends on what address you run some of the instructions from, since they take a byte out of that in account when doing the weird operations, so some bad instructions can be used, if you are running from $fe00 - $feff because it takes the high byte ($fe), adds one to it, then ands something else with it and lastly produces a result that leads to a usable instruction, but only if the and is with $FF so it doesn't drop bits on you. =)
Lots of weird/fun reading there.
It's a shame that I cannot find the other article, because I remember that the author actually had looked at the individual transistors, and figured out the bus conflict or whatever it was, which pretty exactly described what happened, and maybe even why some bits would be more "weak" than others.
EDIT: Oh, hah! Your PDF actually links to it in the same section. The wiki seems currently down, but here's an archive.org link of the page that describes the unstable opcode in great detail. So thanks a lot! https://web.archive.org/web/20210405071521/http://visual6502...
Even stranger: people that insist on writing their (loops; like; ++this) in 2021 as if doing so is in any way superior or something.
In a single-pass run, you must generate code to keep the previous value in case the line actually was foo = ++a; or foo = a++; so that foo gets the pre-inc or post-inc value.
The small part that creates the code for the small "a++" part of the line will then produce a dead store of the pre-inc value, and no 2nd pass will remove it.
++godobject will simply increment that value.
godobject++ will make a copy of that object, returning it to you along with allocating another 50MB pool, purely to increment the value.
only if a is a very simple thing. plenty of "advanced" containers actually do things ; in the "a++" case they have to save the previous state to return a copy, in the "++a" case they don't. If your iterator for instance contains a std::vector<int> to maintain some internal state for a multidimensional dynamic matrix, then in one case you'll get a simple increment, in the other a whole dynamic allocation / free which may be optimized out by the compiler in release mode and definitely won't in debug mode
They are defined as different things. One returns the current value and increments, the second increments the current value and returns it. In the first case you have to cache the previous value, in the second you don't. If your operators don't do that they are bugged and wrong.
The information provided in the article shows beyond any reasonable doubt that both C++ and Rust are exactly equally safe regarding integer overflow.
Rust checks for overflow in debug builds, but it does not check for overflow in release builds.
The same happens in C++. All decent C++ compilers have options for overflow checking, but most developers do not use these options in release builds, for fear that it would affect the performance.
Rust is a little better because it enforces the overflow checking option on debug builds, but C++ is better because the developer may choose to keep the option also in release builds.
So this myth is not busted, it is confirmed.
> C++ is better because the developer may choose to keep the option also in release builds
You can enable overflow checks in release mode if you like: https://doc.rust-lang.org/cargo/reference/profiles.html
The final behavior is decided by the programmer, by using or not using "-fsanitize=signed-integer-overflow" or equivalent options for the compilation.
So after the programmer makes a decision, the behavior in C++ is defined and it is either like in debug Rust or like in release Rust, depending on the programmer's decision.
While I hate undefined behaviors in C/C++, I prefer to be able to make my own decisions on the behavior of integer overflow, instead of being forced by Rust to ignore overflows in release builds.
Edit: Thanks for the Cargo link. If you can change the overflow checking option in the Cargo profile, then there remains absolutely no difference between C++ and Rust regarding integer overflow.
The Rust RFC "Integer overflow #560" is totally misleading in this case.
If you set -fwrapv then they're the same. But that's not standard and not default.
With the appropriate compilation options the C++ compiler will not make any optimizations that depend on the undefined beahvior for overflow.
The only defect here is that the options to do unsafe optimizations are default and you have to give explicitly an option to forbid them, instead of the safe option being default.
Since the overflow check is also controlled by a compilation option in Rust and the default for release builds is no checking, there is no difference. Even if Rust might have done less optimizations, an unchecked overflow will still cause a bug.
And that the option to do the safe thing isn't even part of standard C++ at all!
> Since the overflow check is also controlled by a compilation option in Rust and the default for release builds is no checking, there is no difference.
Rust has no "undefined behavior" option. It only has overflow and trap. That's also a big difference.
Rust either checks for overflow or assumes wrapping, depending on the compilation option "overflow-checks = false/true".
The semantics of the 2 behaviors are incompatible so any Rust program written assuming one of the 2 behaviors will fail in certain cases when it is compiled with the contrary option.
There is absolutely no difference in the C++ case, because the behavior is also controlled by the compilation options.
It does not matter what the language specifications say, when they are overridden by compilation options both for Rust and for C++.
The consequence is the same for both languages. You must write the program assuming a desired behavior, but you must also ensure that the program is compiled with the corresponding option, otherwise failures are likely.
And "most programs are unsafe" vs. "all programs are safe" is decidedly not "absolutely no difference" just because the former could be reconfigured.
For release builds, the default compilation options result in unsafe programs, both for Rust and for C++.
So with default options, all Rust programs are unsafe and all C++ programs are unsafe.
I do not see any difference whatsoever.
It is true that there is one difference in the options required for safe programs.
For Rust you have just one option, to enable overflow checks.
For C++, you must give 2 different options, one to enable overflow checks and one to forbid the optimizations that assume that integer overflow cannot happen.
The extra flexibility of the C++ compilers is actually useful, because there are cases when you are certain that overflow cannot happen and in that cases you want the compiler to do all the applicable optimizations.
The right way to use such options is to always enable the safe options, which will ensure correct behavior for the program in all cases and to disable the checks and/or enable the unsafe optimizations only for the cases when it can be predicted for sure that overflows will not happen.
The failure cases for mistaken wrapping and "mistaken undefined" (the latter meaning that the compiler is allowed to assume that overflow cannot happen) are not different.
If an unexpected overflow happens, that is a certain failure in both cases.
When your program is written to work with modular numbers a.k.a. wrapping numbers, that is a different case and in that case enabling overflow checks will break that program.
Interval numbers and modular numbers are distinct types that cannot be interchanged in a program whenever interval overflow or modular reduction may happen.
> The failure cases for mistaken wrapping and "mistaken undefined" (the latter meaning that the compiler is allowed to assume that overflow cannot happen) are not different.
They are extremely different. You still get a number with overflowing, and most of the time it's the number you want. If you expect wrapping, it will always be the number you want. Undefined behavior can be much more broken, and it will actively defy expectations.
No language will force you to write correct code. Safety is related but not a synonym for correct.