83 comments

[ 3.0 ms ] story [ 177 ms ] thread
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.
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.
I agree that it's probably what they think. My opinion is that flagging is unecessary in that case since people can already downvote and hide distracting comments.

Thanks for your answers.

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)

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

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

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

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.

I think I get what you're saying. The practical benefit of defining signed integer overflow in C, without any other changes, might not be very big for most C programs, since they're likely to get an out-of-bounds access either way. I'd add to that that undefined signed overflow makes it much harder to write good C code. Even if you carefully check all your array bounds, an unexpected integer promotion somewhere in there might lead the optimizer to delete the check you wrote.
C is a strawman though when we're discussing C++. Checking array bounds in C (or I'd argue, pretty much checking anything in C) is just a losing battle. The lack of proper abstraction facilities perpetually fights against you. C++ actually lets you abstract checks away into the definitions so that you don't have to modify every usage site.

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!

[1] https://gcc.godbolt.org/z/i-FMV-

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++?
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++?
Different type? Sure. Disadvantage is that it’s not pervasive. Compiler flag? Probably, I am not an expert on compiler flags that change language semantics; we don’t do that in Rust, but I know C and C++ compilers do.
> Compiler flag? Probably, I am not an expert on compiler flags that change language semantics; we don’t do that in Rust, but I know C and C++ compilers do.

What do you mean? Is Rust's -C overflow-checks just a joke then? https://doc.rust-lang.org/reference/expressions/operator-exp...

Okay, so let's review the language's rules here:

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

So let's review where we are right now. C++ left the entire thing undefined, which gave compilers the complete freedom to add flags that define e.g. wrap-around or trapping or other behavior. I suggested you can use such flags if you want. You complained about that and said it's "changing" language semantics... despite the fact that it's 100% consistent with the "rules of the language". You even went on to claim "we don't do that in Rust". All right. So I pointed you to a Rust compiler flag that changes the default wrap-around to a panic. Now you're saying that's not changing Rust's language semantics... and your defense is "it's consistent with the rules of the language"...?!
Undefined behavior is different from implementation defined behavior. Code that exhibits UB is not a valid program. Flags that take UB and define it are categorically different than flags that tweak various options, because flags that define UB expand the set of valid programs, where flags that tweak options do not.
Okay, you're very confused. You're getting it almost backwards.

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

I do absolutely understand that UB does not prohibit implementations from defining something. That’s part of the core of what we’re taking about! I’m saying rustc chooses to never do so. C and C++ compilers do choose to do so. That distinction is what we’re talking about.

(As well as that overflow is UB in one language and not another, of course.)

No comment on everything else I wrote beyond that 1 sentence? Your positions are quite contradictory, which is what I've been trying to explain in the entire comment.
I just don't think we're going to get anywhere, but wanted to correct the assertion that I do not understand this aspect of UB.
I mean, your positions are contradictory, but sure.
Compiler flags are not part of the language, so not portable.

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.

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?

That was the example given in the original article
Yes, but I was asking about your experience.
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.
(comment deleted)
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.

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.
(comment deleted)
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.

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