The C++ Killers (Not You, Rust)
hackernoon.com
hackernoon.com
I do think that Rust helps you squeeze out that last 1% of performance over C++. Because instead of spending all day finding that one line of code where an unsafe threaded memory access leads to a race condition bug, I can spend more time performance profiling and optimizing the hot spots.
Also I'm not sure how the article author thinks Python libraries & tools can beat C++ but disregards other languages for not achieving the same (or better) out of the box. If Python can be made to compete with C++ any language can, and almost any other language is more preferrable to me than Python when it comes to production code.
1) A cool research language packaged into a VS Code plugin 2) A cool research compiler for Python + numpy 3) A cool generic ISA project
will kill C++? Maybe for the author's _very_ specific use case of blindly copying things from SymPy, but really I think you'd have better odds betting on a D resurgence.
I wish we had an ISA with non-linear, two-level memory space, so you can have non-overlapping arrays for free (well, as free as virtual memory required to support that would be free): that would mean that e.g. realloc() will never have to call memmove(), range checks are inescapable since they're baked into hardware, etc.
But implementing (and using) C on such an ISA will be deeply unpleasant, so it will just die because if something doesn't run C efficiently... it won't be used.
I was going to drop a link to back that up and was shocked to find it in the bible:
https://www.forbes.com/sites/quora/2015/05/26/when-is-java-f...
This is pretty basic C++/computer architecture stuff.
> They just don’t give you a competitive advantage over C++. Or, for that matter, even over each other. Most of them, for instance, Rust, Julia, and Cland even share the same backend. You can’t win a car race if you all share the same car.
But it's the same backend that C++ uses...?
> Because if you can write in Python and have the performance of C++, why would you want to write in C++?
I'm no rust fanboy, but isn't this the argument he used against Rust in the first place?
Given the choice I'd rather program in anything else.
I wish "clever but bad" programming games were more common in every language. It takes care of the learner's need to push limits, but is a pleasantly humorous reminder why we don't do that in production.
I'm questioning OP because I have strong doubts that he actually understands C's integer promotion rules that he can claim that it's basic stuff. They are very subtle and even C and C++ experts get bitten by them.
If you ignore signedness, the rule is extremely simple and natural: numtype = max(type(a), type(b), int), where char < short < int < long < long long < float < double < long double.
Signedness throws a wrench into the works though.
There is a reason that the author explicitly states that MSVC is the compiler that produces this output, because clang and GCC do not do so, even if they run on the same computer architecture. This is due to the fact that both those compilers can recognize that signed integer overflow is undefined behavior and leverage this fact.
As others have said, to consider these details just basic things is absurd. Almost no one knows this stuff, including you.
I looked into this and you're totally right! But it actually didn't until relatively recently. Other representations were explicitly permitted by the standard at least until C++14 or so, what I said should be interpreted in that context. (And in fact, the standard requires it _because_ architecture was already universally designed that way, which I know you already know.) Thanks for the information though, this is good to know.
> The reason for the negative result has nothing to do with computer architecture
An existing conforming implementation will produce the bit patterns given in the article because of the architecture of the machine it's running on, so that's why I mentioned architecture.
> and has to do with C++'s integer promotion rules which promote an unsigned 16-bit integer to a signed int.
For sure, it also has to do with that, it just wasn't relevant to the "architecture" part of my comment which I was explaining, so I omitted it. I think we're saying the same thing from different points of view.
> As others have said, to consider these details just basic things is absurd.
I guess I'm inured to it, having worked at this level of abstraction for a long time haha. This is why progress is made one funeral at a time I suppose D:
> Almost no one knows this stuff, including you.
There are a lot of things I don't know, and this may be one of them!
Thanks for your responses.
Well, it would be if they'd written it correctly—that should be a multiplication, not an addition. As written, it would certainly be very mysterious, but mostly just because it isn't true.
The C rules for integer type promotions are incredibly hard to reason about. They should have just banned implicit conversions from the beginning. This is not super basic stuff.
That problematic value is produced by multiplication (*), not addition (+). It happens because uint16_t is unsigned short, and all arithmetic must be promoted to at least rank int.
The easiest universal fix for proper signed/unsigned arithmetic promotion is: (0U + uint16_t(50000)) * uint16_t(50000). See https://stackoverflow.com/questions/39964651/is-masking-befo...
> And suddenly it turns out that all the “C++ killers”, even these which I wholeheartedly love and respect like Rust, Julia, and D, do not address the problem of the XXI century. They are still stuck in the XX.
You forgot to mention Carbon ( https://en.wikipedia.org/wiki/Carbon_(programming_language) ) and cppfront ( https://github.com/hsutter/cppfront ).
FTFY
Multiplication of intX_t by intX_t should produce int2X_t which is btw is what actually happens in hardware! And detection of overflow of addition/multiplication in a sane language should not require ridiculous algebraic acrobatics that involve additional additions/multiplications (or a division, yeah, I've seen that too).
template<size_t N> operator*(int<N>, int<N>) -> int<2*N>
instead of template<size_t N> operator*(int<N>, int<N>) -> int<min(numeric_limits<int>::width, N)>
? Why or why not?But again, all your arguing about "numbers should be closed under arithmetic operations just as they are in C/C++" is kinda pointless because in C/C++, they are not! When you add/multiply two signed char's, or two shorts, you get an int, not a signed char/short, so only ints and longs are actually closed under arithmetic operations. Whoops!
C++ is doing implicit integer promotion of integer variables with types smaller than int, and that promotion converts those values (of the operands of the +) to int (gross generalization yeah yeah).
So the result on g++ amd64 will be 100000 (as you would expect) if int is more than 16 bits (nowadays it is), even WITH `(uint16_t(50000) + uint16_t(50000))`.
I've also tried it in MSVC 2010 and it says the result of `std::cout << (uint16_t(50000) + uint16_t(50000)) << std::endl` is 100000 (both on win32 and on x64).
Try it on an arduino and you will get 34464 (with g++ targeting 8 bit atmel).
Think you want implicit integer promotion in a systems language? You really don't. They are an unnecessary language feature.
Also, the article is only tangentially about that--that's just an intro. The actual body makes very good points, and I think it's more than a little tongue-in-cheek :)
uint16(50_000) * uint16(50_000) is uint32(2_500_000_000), which turns out to be int32(-1_794_967_296), the garbage result the author cites.
If int was 16-bit (which it is allowed to be), no integer promotion would take place, and the result would be 50000^2 mod 2^16 = 63744.
It didn't used to be like that. Memory-related crashes and vulnerabilities weren't blamed on C++, but just bad programmers not following best practices. Any discussion of flaws in C++ was a circular "if you don't like C++, then use C", and "if you don't like C, then use C++", because there wasn't a third option.
Now even the C++ community is wondering whether C++ is becoming a "legacy" language.
Because then you'd have to write in Python?
Seriously, all languages have tradeoffs. Performance is hardly the only consideration.
The same is true in low level software, if you're really interestied in performance it doesn't matter if you express yourself in Rust or C++ what you're really doing is trying hard to point the compiler to one best solution- a solution you know through either experience or experiment. If you care less about performance, pick a higher level language and trust the compiler more. If you're just trying to solve a problem and don't need performance at all (any python script, or building the worlds most popular code editor) just stick to python or javascript or something.
1: Unsubstantiated and vague statements, e.g. X is 50% faster than Y, well, how is it measured, what are the actual numbers?
2: Measuring performance by measuring how much time a program takes, e.g. X takes 3 seconds while Y takes 6 seconds, well, is that consistent across measurements, have you measured it on different machines, have you measured that under various different conditions?
Various factors can impact the apparent performance of code, among which is memory layout. Considering on GNU/Linux (and maybe other platforms) the global environment is passed into a program by putting it above the stack (remember, stack grows down) the memory layout can differ by virtue of having different values for the environment variables USER (username), HOST (hostname), and PWD (current working directory); apparent performance is not really reliable.
I don't know of the best practices for measuring performance, but if I were responsible for measuring the performance of a program I'd do so by looking at its disassembly, assigning a cost to the different types of instructions, and adding the costs together. That probably is a terrible way to go about it, so I welcome any insight into why that isn't done.
I was very confused by this claim before I realized it should be *, not +.
Either way, the operands are promoted to int before performing the addition or multiplication. But with addition, this is not too surprising, as you just get the mathematically correct result 100000. With multiplication, you get 2500000000, which is too large for a 32-bit signed integer. This is undefined behavior, but in practice it often results in wrapping, which is where you get -1794967296.
As a result, it is nearly impossible to write platform-independent code that does something like hashing (where wraparound is intended) while also producing consistent results across platforms. If course in reality, it's the other way around: 64-bit platforms were forced to define `int` as a 32-bit integer, because otherwise they wouldn't be able to run any existing code.
C23 finally fixes this by introducing new fixed-size integer types _BitInt(N) that don't suffer from this numeric promotion mess. But of course the typedefs everyone is using will have to stay broken.
If Spiral spotted the problem automatically that's very cool, but that's elimination of a specific bottleneck, and that is possible to do manually too. The article made it sound like a 2x superoptimizer for arbitrary algorithms, but that's likely too good to be true.
Love that.