Debunking that C++ is faster and safer than Rust(viva64.com) |
Debunking that C++ is faster and safer than Rust(viva64.com) |
Not having undefined behavior does make life easier, but having it be defined and then giving the example which benefits from the way that Rust chooses to define it is not really fair.
> With less effort, Rust generates less assembly code. And you don't need to give any clues to the compiler by using noexcept, rvalue references and std::move. When you compare languages, you should use adequate benchmarks.
Actually, the issue is that C++ just can't match Rust's semantics here. By default it will allow for exceptions, by default it will copy; if you watch the talk rvalue references cause the double indirection and fixing this would require some changes in the language to accommodate the use case.
I'm actually struggling to see what the practical benefit is in having it wrap around. The program is still producing garbage at that point, which you're not handling, so why not let the compiler just forget about that case just like you already did?
You aren't supposed to rely on this semantic, as it's an error. If the checks get cheap enough, rustc will also check in release.
There's a pretty big practical difference between "getting an unexpected numerical result" and "letting an attacker steal my TLS keys and mine bitcoins on my machine."
I have mixed feelings about this. It seems like many examples of undefined behavior are things that don't make sense to do (ie ought to result in an error) which would often be undecidable at compile time. Runtime error checks would incur performance penalties so it's up to to programmer to include them.
The borrow checker is an obvious advantage here. Beyond that, I guess you could do many of the same things in unsafe blocks and they could cause problems just as easily. If they end up breaking things no one will blame the language because the tin was clearly labeled.
I do prefer language designs that make it possible to write things that are safe by default. It just seems like many problems are misattributed to undefined behavior but are actually due to systemic issues in the design of the language.
Which is why in rust, there are many runtime checks done in debug builds but not in release builds. (Including checking for wraparound and bounds checks).
The idea being that automated tests should catch the errors, assuming you wrote good tests.
Be aware that this isn't a LLVM bug but a direct consequence of the insanity of C++ specification (wrt. forward progress induced undefined behaviour).
The C++ rules around forward progress allow C++ compilers to faster eliminate code which doesn't produce any observable side effects (without the code triggering undefined behaviour) but it also removes code which intentionally or not hangs the process in a busy loop or is intended to cause a stack overflow... (e.g. for testing protections).
The flag currently isn't added to rust as the penalty effect on compiler time (needs to run more analysis) and runtime (doesn't eliminate all code it should) is currently pretty high.
So this might take a while until _fully_ fixed (you always can pass in the flag yourself if you want).
Through some fixes which make it harder to hit the bug until a proper solution is found _might_ not be so far of (I hope).
C++: https://godbolt.org/z/VCf638
Rust: https://rust.godbolt.org/z/jRFiw_
I think the key difference here is that C++ allows specializing optional on trivial types - can anyone shed more light?
Is this a remnant of #[inline] on Option's Clone impl methods?
With "larger" types (e.g. an optional several-GiB array) it seems like this could save some time depending on where things are in memory.
Anyway for a truth (well maybe it is a myth?) that we can't bust yet... Rust is simply not available on all platforms that C++ is. Two platforms that I think are missing:
* 16-bit MS-DOS
* 32-bit PowerPC (Linux)
That's not entirely true - you actually can create .COM executables: https://github.com/ellbrid/rust_dos
I mean 64-bit PowerPc is by now around 17 Years old and even 15 years ago some of the most widespread users of PowerPc (Xbox) switched to using 64 bit PowerPc architecture...
> Both C++ and Rust have generated identical assembly listings; both have added push rbx for the sake of stack alignment. Q.E.D.
seems to be completely wrong: a decent compiler is able to align the stack without "touching" it. For the variables inside of the function to be pushed to the aligned stack position, only different offsets have to be calculated. For the stack itself to get to be aligned, only the register has to be updated, surely nothing has to be pushed.
So something else must have been happening there, and I don't have time to analyze what, but I'm sure push is surely not necessary for alignment alone.
They way it's done is that if you use a certain stile of C the compiler will speculative do assumptions about the code allowing it to basically add all the C optimizations. Except that it always has to check if the assumptions are uphold and then fallback and that because it's a JIT it has much less time to optimize the code and do cross-code-section optimizations.
But even with good language design the compiler need to use them, which needs time to be implemented etc.
So in practice it's often more a mixture between how easy/hard the language makes optimizations and how much work (with given expertise) was put into the compiler optimizations.
Through there are insane optimization which need to high amount of knowledge about the code and as such which you will have a really had time to ever realize with Asm,C or similar. But most time they aren't worth it as getting them right is hard and the time is often better spend with adding more straight forward optimizations, maintaining the compiler code etc.
int bar(int a, int b) {
int z = a * b;
foo();
return z;
}
https://godbolt.org/z/a8Y35rWhere to put `z`? It has to be someplace where `foo` won't clobber it, like a callee-saved register - in this case ebx.
But now `bar` is responsible for saving the value of ebx for its caller. That explains the push and pop.
Alignment is unrelated; this is just about calling conventions.
Also just to be clear I don't say that rust isn't for embedding, just currently development is focused around server (and kinda desktop) usage with some focus on some of the most wide spread embedding targets.
I hope in the future rust will be the go-to alternative for C/C++ in any use-case (at least any which llvm supports). But we are not there yet. But we are slowly getting there step by step.
Well arrays are bounds checked by default in Rust, so you can't do that one. You're more likely to hit a crash, which I think is decidedly better than the compiler deciding to optimise out or rewrite your function because it contained UB.
But that's my entire point. If you're observing a benefit here, it's decidedly not due to the wrap-around, but due to other features (like bounds checking), and the argument should be that those features make Rust better than C++. It seems strange to give praise to the multiplication wrap-around instead.
The original comment from simonw (https://news.ycombinator.com/item?id=23137531) originally did not have many comments but was already flagged when I saw it first, which was strange to me because the comment basically says "this is a bad title (it distracts from the content), don't do that". This is quite a reasonable comment to me.
So under that comment are many exchanges that may or may not be offtopic, but I was curious in particular about why that parent comment was flagged, and what recourse users have when something is flagged but maybe shouldn't (wasn't there a "vouch" action?). My opinion is that if the submission itself brings political subject into light, even indirectly, it is unfair to flag political comments as offtopic.
Thanks for your answers.
I already don't run into such cases though? Do you run into cases where changing an overflow to wrap-around (e.g. maybe via unsigned?) makes it suddenly your code work on 2 machines, whereas leaving it as overflow makes it work on 1 machine but not the other?
Regarding optimizers deleting your checks though: is that something you encounter in practice, or just something you see people ranting about in blog posts? Can you even trigger this behavior if you try? Have you seen it happen more than once in a blue moon? I know on my end it's either never happened to me (likely) or it's been long enough ago that I have no memory of it. Even when I actively go out of my way to make this kind of thing happen, it gives me a hard time. Even the most blatant examples you'd try don't end up getting optimized out like this. Try [1] for example. It's both out of bounds and an uninitialized read, and yet the check is still there. If anything it's incredibly disappointing how bad optimizers are at optimizing out bounds checks!
What do you mean? Is Rust's -C overflow-checks just a joke then? https://doc.rust-lang.org/reference/expressions/operator-exp...
* There are two modes for overflow checking: enabled, and default.
* If overflow checking is in "enabled" mode, overflow results in a panic.
* If overflow checking is in "default" mode, the results are two's compliment wrapping.
* Debug builds are "enabled" mode.
(There's a few other details, but this is good enough for our discussion. See https://github.com/rust-lang/rfcs/blob/master/text/0560-inte... )
Rustc implements this as follows:
* When debug_assertions is on, it's in "enabled" mode.
* Otherwise, it’s in “default” mode.
* -C overflow-checks turns on "enabled" mode, regardless of other settings.
This is completely consistent with the rules of the language. If these checks ever get cheap enough, rustc may even start to turn them on by default, which is also acceptable under these rules. We'll see if that ever happens, though.
Using a different data type? You'd have to use arbitrary precision numeric data types to avoid this. After all, you can still overflow 64-bit ints.
Undefined behavior is behavior that the language does not define, not behavior that the implementation is prohibited from defining. That's why you can't treat it like (for example) a random-number generator. Implementations are well within their rights (i.e. 100% consistent with the language) to define previously-undefined behavior to be anything. They're also just as welcome to leave them undefined. Both are 100% consistent with the language and neither is "changing" the language semantics. No program that exhibited UB is going to misbehave somehow just because someone decided to define the behavior under UB. The semantics already allowed anything to happen. Whatever they define falls under "anything could happen".
Implementation-behavior is behavior that the the implementation is guaranteed to define. Like with UB, the implementation has freedom to choose a behavior. Unlike with UB, it is not allowed to leave that behavior undefined. So the program can be sure to have a well-defined output.
It is completely wrong to simultaneously say changing "wrap" to "crash" is "not changing semantics" and somehow "consistent" with the language rules, but that changing "undefined" to "wrap" is "changing semantics" and "not consistent" with the language rules. If the language wraps on overflow, then changing that to a panic is actively shrinking the set of valid programs; valid programs that used to behave one way now behave differently. (They crash!) The latter is merely expanding the set of valid programs; programs that had no rights to claim any behavior under UB now actually have a right to claim something under the implementation, but that doesn't change the behavior of previously-valid programs. They're still provided exactly the same guarantees they already were.
(P.S., UB isn't even a property of a program, but of an execution. But I'll leave that out since the simplified version is clearly confusing enough as-is.)
(As well as that overflow is UB in one language and not another, of course.)