Hacker News new | ask | show | jobs
Debunking that C++ is faster and safer than Rust (viva64.com)
143 points by payasr 2230 days ago
9 comments

> As you can see, the documented behavior and the absence of undefined behavior due to signed overflows do make life easier.

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.

> the example which benefits from the way that Rust chooses to define it

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?

The practical benefit is not in the wrapping specifically. The difference is in it being UB vs not UB; that is, overflow is a "program error" not UB, and so the language defines how implementations must handle this error.

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.

I mean, the post seemed to be arguing that the wrap-around is somehow beneficial, so I'm not sure you're saying the same thing. But if all you want is check + crash when that happens then can't you just use a compiler flag or a different data type or something else for that in C++?
It is beneficial that the behavior is defined, and therefore consistent. So you don't run into cases where it works on your laptop, but not on your server. Also, in this particular case, overflowing is probably not terrible, since you are computing a hash code. The worst case is that the distribution may not be as even as you hoped.
> It is beneficial that the behavior is defined, and therefore consistent. So you don't run into cases where it works on your laptop, but not on your server.

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?

I’m not saying that the wrapping behavior is beneficial. I’m saying that having it be not UB is beneficial.
But if all you want is check + crash when that happens then can't you just use a compiler flag or a different data type or something else for that in C++?
I mean, if you're doing two's complement arithmetic as the example relies on then it's certainly useful. If you want any other behavior, then it is not. (Rust's differing behavior between different optimization levels IMO makes it basically impossible to use usefully, FWIW.)
How likely is that "if", though? How often when you see signed multiplication in code do you find people are actually paying attention to the negative results and planning around 2's complement behavior? I know there are occasions in which people do this, but it's so incredibly rare compared to when they multiply two numbers. And on the rare occasions when the bit patterns actually matter to them and they're paying attention, it's not like they have no way to solve the problem in C++.
If it wasn't clear, I'm saying that making the behavior defined is nice, but personally I think defining it to wrap is not the best way it could have been done. IMO a consistent trap would have been nicer.
> what the practical benefit is in having it wrap around

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."

How sure are you that that getting an unexpected numerical result isn't going to let an attacker take over your machine? In most code I see the behavior is just undefined and not paid attention to by the programmer, regardless of what the compiler is doing. A huge fraction of the time you'll just step out of the bounds of an array, just deterministically instead of nondeterministically. People don't pay attention to what happens in that case regardless except in like 0.01% of the time when they're writing some kind of bit manipulation magic, and in those rare cases they can handle them in C++ too, with a custom wrapper or an unsigned type or something.
> A huge fraction of the time you'll just step out of the bounds of an array, just deterministically instead of nondeterministically.

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.

> Well arrays are bounds checked by default in Rust, so you can't do that one.

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.

There is a real advantage to wraparound in that one can understand add, sub and mul as ring operations (as in common arithmetic), which makes it easier to understand the semantics of more complex expressions.
> Not having undefined behavior does make life easier

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.

> Runtime error checks would incur performance penalties

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.

Bounds checks are absolutely done in release builds. The optimizer is more likely to be able to remove ones that are irrelevant, but the checks are not turned off.
> The bug has been present in LLVM since 2006. It's an important issue as you want to be able to mark infinite loops or recursions in such a way as to prevent LLVM from optimizing it down to nothing. Fortunately, things are improving. LLVM 6 was released with the intrinsic llvm.sideeffect added, and in 2019, rustc got the -Z insert-sideeffect flag, which adds llvm.sideeffect to infinite loops and recursions. Now infinite recursion is recognized as such (link:godbolt). Hopefully, this flag will soon be added as default to stable rustc too.

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).

It's indeed not a bug for C++, but to be clear it is a bug for C and Rust that use LLVM but don't have that same guarantee as C++. LLVM assumes that guarantee holds for all frontends, and added the sideeffect opcode so that frontends for languages that don't have that guarantee have a way out.
Important to note that PVS-Studio is a static analysis tool for C++. As such it would definitely be in their interest to have more people use C++, but since they're arguing in favor of Rust here it definitely speaks to the fact that their analysis is unbiased.
They have also previously posted some negative ones too, so I was surprised to read this for that reason. I think it also contributed to my confusion around the title vs text, that others have expressed in this thread.
Here's a case I stumble on: Rust seems to generate unnecessary branches. Compare copying an optional range:

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?

It looks like the C++ version always does the copy, regardless of whether or not the optional is empty, whereas Rust only bothers copying if there's anything there.

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.

This article is a bit click-baity. It's more about busting myths by a particular C++ programmer against rust.

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)

> * 16-bit MS-DOS

That's not entirely true - you actually can create .COM executables: https://github.com/ellbrid/rust_dos

That's 32-bit DOS, not the same.
How much active development of new software is being done for MS-DOS? Really curious to know.
I'd hope that he was just joking with that list
I’m sure there’s some POS systems that are maintained on MS DOS.
Is anyone besides embedded doing _any_ 32-bit dev outside of maintaining legacy code?

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...

Xbox 360 still ran in 32bit mode with the exception of the hypervisor. No use of 64bit pointers on a system with a max of 1GB of RAM other than just wasting cache space, and there's no real other benefit tacked on like you see in other 64-bit archs.
Thanks I didn't know this. I just looked up since when PowerPc 64-bit is a think.
Ah. In that case the PowerPC 620 was a 64 bit design from the late nineties.
Depends what you mean by "embedded," but almost every set-top box and TV has a 32 bit ARM SOC. These run software under active development: web browsers, Netflix, etc.
Yup, I included set-top into embedded, too.

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.

Embedded sounds like a great domain for Rust that it sadly often cannot really be used in.
Rust is promoted all the time as the sensible choice for embedded work instead of C or C++.
The thing is “embedded” is an extremely broad space. There’s a bunch of Rust stuff going on in some corners, and absolutely none in others.
Yeah, to clarify, it’s often touted as great for embedded but wholly impractical, it turns out, for some things. Which can be alarming and unexpected
Very interesting article. Most of the time I do not like myth busting articles because they are to much focused on opinions and taking things out of context but this one is very well written and fully based on facts on both sides. Thanks for sharing.
Thank you!
Just looking from afar, I don't have time to analyze every other claim, but this one:

> 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.

Counterintuitively, on some x86 architectures, push ¿is/can be? faster than decreasing the stack pointer’s value because the CPU uses dedicated hardware to speed up subroutine calls (https://stackoverflow.com/a/36633556)
It may just be for alignment. A push may be just (having a specific case in the CPU) as fast as updating the SP.
You are correct, it has nothing to do with alignment. rbx is a callee-saved register, so the callee saves it.
Do you mean thar rax is caller-saved?
rax is indeed caller-saved, but I was referring to rbx. In this code:

    int bar(int a, int b) {
       int z = a * b;
       foo();
       return z;
    }
https://godbolt.org/z/a8Y35r

Where 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.

This is an endlessly perplexing headline, as its core assertion, "C++ is faster and safer than Rust", is what the body of the article spends its whole time attempting to refute. A more accurate title for the content of this article would be "Debunking the myths that Rust is not safer or as fast as C++".
Yeah I was really confused too. You expect that author to bash Rust in favor of C++, but he does the exact opposite.
Some scare quotes might do the title some good, too.
Ok, we've debunked the title above.
Are comments about the title of articles offtopic? Because sometimes comments that discuss titles, without being inflammatory, are flagged, and sometimes they aren't.
I suppose they are in principle, but we don't have an explicit rule against them, and user passions around titles burn so hot that it wouldn't make a difference if we did. There are also so many of them that you're not likely to see consistency around what gets flagged vs. not. Are there specific links we could look at?
Thanks for replying. I was thinking about the "Make X Y Again" types of article that, intentionally or not, invites offtopic discussion.

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.

A bunch of users flagged that comment. We can only guess why users flag things, but my guess is that in this case they thought it was a specimen of the very distraction it was worrying about.
Clickbait for software engineers.
Strictly speaking it's the compiler that generates faster code.
You'll never get faster code out of JS, though. Language design matters a lot.
Well, there are some close to native code speeds for some (very constrained) use-cases.

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.

Sounds like the 'sufficiently smart compiler[1] myth.

1. https://wiki.c2.com/?SufficientlySmartCompiler

It's both. With bad language design you won't get fast code (at least without going through insane loops).

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.

They both depend on the same compiler backend, so it's really the language here.