63 comments

[ 0.21 ms ] story [ 66.5 ms ] thread
Assembly is faster and safer than rust. Rust is a fad it will fad.
also ++C is faster
Only C++ programmers will appreciate this joke.
You jest, but I always found it puzzling that there was a time (supposedly?) that compilers were dumb enough to assemble ++c and c++ differently in cases where they simply occur as stand-alone expressions, despite their obvious semantic equivalence.

Even stranger: people that insist on writing their (loops; like; ++this) in 2021 as if doing so is in any way superior or something.

Optimizing integer increment might be easy. Hard thing is optimizing overloaded increment operator. ++c is naturally faster, because it does not have to make a copy of its previous value. Especially if that operator implementation is inside another library. So it makes sense to use fast-by-default approach.
Yeah, I should have specified that I was referring to plain-old-data types such as integers. Custom objects overloading their operators certainly allow any number of differentiated rabbit holes to be dug.
(comment deleted)
With complex iterator types that carry additional state themselves (for example think iterators to sparse data or generators), it definitely makes a difference
a++; means do nothing then increment a, ++a means increment a then do nothing, what should be the difference? (Of course you can define operator++ to format the hard drive. )
If you have a single-pass compiler (for speed) then it will not know on pass #2 that the old value is to be used for something or not and can drop it.

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.

Allow me introduce operator overloading, where I have suddenly defined a ++ operator on my god object that contains a 50MB pool. This operator increments a single field in it, and I really need it because I do it so often.

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

That's what I meant with operator++ being redefined to do anything.
but when you write generic algorithms (as you should), you don't know beforehand with which "operator++" your a will be called. So you have to go for the one which will give the best performance.
> a++; means do nothing

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

That is increment a part, for whatever is being defined as incrementing for this class. If you define different things for post and pre increment they will cost differently, sure. If the compiler decides to do more than what I told to, it will be longer. But will a++ always cost more than ++a?
> If you define different things for post and pre increment

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.

What measure of safety has assembly basically ever win?
There's no undefined behaviour in assembly language.
I invite you to search the ARM reference manual (the so called "ARM ARM") for the terms UNKNOWN and UNPREDICTABLE. There is also actually UNDEFINED, but in the context of ARM it means that a specific exception happens, so it's pretty much the most PREDICTABLE of the bunch.
The old VLSI Technology Inc (later Philips, now NXP) reference manuals for one of the first ARM processors (the one used in the Acorn Archimedes 305, 310) mentioned that the algorithm used for cache eviction was not something relatively predictable like LIFO or FIFO but essentially random. Which in theory means that if you were 'unlucky' then your program would run slower than for a luckier person etc.
Ha. Though if you're seeing a persistent difference over time, thousands and millions of evictions, your luck is already so immaculately bad that you should expect the power to go out every time you try to run a program.
As Computer Engineer: oh well, do I have bad news for you ;-)

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.

Indeed, best to avoid any eye contact with the FPU at all.
Only if you define "undefined behavior" as in a very constraint sense (as in "the language specification doesn't specify the behavior").

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.

6502 is extra fun, because it has "unstable" instructions, whose result would be dependent on specific chip, temperature, and other factors. I can't find the article that dissected some of them anymore, but as I recall it was because of stuff like an (internal) bus conflict with multiple drivers enabled (EDIT: close, but it's even wilder than that).

EDIT: Thanks to IcePic's answer, I found it here: https://web.archive.org/web/20210405071521/http://visual6502...

There is a writeup in PDF here https://csdb.dk/release/?id=198357 about what actually happens on all the unimplemented instructions that still "run" parts of the CPU, some with decent results, and some with less.

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.

Thanks, but I meant something even more unpredictable, which is hinted at in your document: "ANE [...] is chip- and/or temperature dependent" and "can not be reproduced in visual6502, which hints on it being some analogue side effect that the simulation does not cover".

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

Yeah, the language that:

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

Clickbait title responding to yet more clickbait. I'll save you the trouble: "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." (Yandex)
Clickbaity title but nevertheless I've found the content interesting.
TLDR; From article:

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

A better title would be "Mythbusting the idea that C++ is faster than Rust" or something like that.
Rust really is such a special and welcomed language, I hope it’s able to pick up even more market share than it has already, particularly in game dev!
First check speed/memory usage. Then the one which is easier to code in. And then third : easier to integrate with the OS and build library functions which will be consumed by other apps written in higher level languages.

These are the three things that matter most.

Different languages strengths "matter the most" in different usage scenarios. Sometimes an OS doesn't even exist. Sometimes an application is written fully in the same language. etc.
Misleading title. Correct one - Busting the claim that C++ is faster and safer than Rust.
(comment deleted)
The article does not provide any valid argument against "Myth 1. Rust's arithmetic is no safer than C++'s".

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.

The generate the same assembly in this particular context, but changes to surrounding code could change that. The fact that signed int overflow is UB in C++ is a major difference.

> 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 fact that integer overflow is undefined in C++ is ugly, but this leaves the solution of this problem to the compiler.

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.

Rust integer overflow is never undefined behavior. It'll either wrap (default release mode) or trap (debug mode, release mode with the flag on). C++ defaults to it being undefined.
Rust also trivially allows to keep overflow checks in release builds.
If you give a C++ compiler the opportunity, it will change the assembly into something that gives mathematically impossible results in the name of optimization. It might even generate assembly that crashes on certain values. That's what busts the myth.

If you set -fwrapv then they're the same. But that's not standard and not default.

That depends on the compilation options.

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.

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

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.

There is no practical 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.

The failure cases for mistaken wrapping and mistaken undefined are very different.

And "most programs are unsafe" vs. "all programs are safe" is decidedly not "absolutely no difference" just because the former could be reconfigured.

I cannot understand your argument.

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.

With default options, Rust programs overflow in a limited way most people call 'safe', and C++ programs that would overflow have undefined behavior which is very unsafe.

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

So many rust-fans triggered. It's comical. They've been trying to kill c++ since the 90s, get over yourselves.
C++ is sooo much better than any other compiled language! For example, using std::cout to print the value 97 will produce different results depending on whether this value is of the type int8_t or int32_t (but in Rust, printing 97 always outputs "97" without depending on the type of 97 that can be i8, i32, whatever). This is because C++ uses C headers to typedef int into int32_t (or whatever is 32 bits on the implementation that is being used) and to typedef char into int8_t (so the value 97 of type int8_t is printed as "a"), at least that is the case with glibc. What a good language, in comparison Rust is so boring and uninteresting.
By "glibc" i meant the GNU C++ standard library implementation, not glibc
(comment deleted)
C++ is the language I use most for my day job. I am definitely on the "love" side of the "Marmite" argument.

What is scary is, I don't doubt that some people will read your comment and think it could be true!

Don't get me wrong, C++ is interesting to work with ^^, but what I said is true (just go ahead and test it, `#include <cstdint>`, then initialize a variable of type `int8_t` and `std::cout` it, I just tried it on godbolt.org with the `x86-64 gcc 11.2` compiler and it printed the value as a character rather than as a number (unlike an `int32_t`)). I never miss an opportunity to mock C++ and its many layers of features that don't always interact well with each others. Maybe take it as a fun fact and not as an attack..
You're right! That's the thing with C++ - you learn something new everyday.

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.

Actually when I said that in comparison Rust is boring, I was only half joking. C++ is way more fun in the sense that it feels like a patchwork with surprising interactions that can get even C++ masters. C++ dev sounds like a fun job ^^
(comment deleted)
So, first of all, this was posted on HN last year: https://news.ycombinator.com/item?id=23134688

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.
Well, for one, in the Rust version, check(i32::MIN) will return false if built with overflow checks disabled (the default for release builds).

C++ will of course always return true.

Right, wrt. C++ the "square" operation on signed integers makes the whole program undefined if overflow occurs, whereas the Rust equivalent is defined to wrap using two's complement arithmetic. And of course the square of INT32_MIN cannot be represented in int32_t.
Indeed, but then you are partially "un-rustifying" things. That is, the author specifically boasts in the article that:

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