C and C++ are most often used when performance is the highest priority. Undefined behavior is basically the standards committee allowing the compiler developers maximum flexibility to optimize for performance over error checking/handling/reporting. The penalty is that errors can become harder to detect.
It appears the author is a Go advocate. I assume they are valuing clearly defined error checking/handling/reporting (the authors definition of correctness) over performance. If that's what you are looking for, consider Go.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue.... Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels? Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
Hum... We have to move further than this citation. The 1980s C was much more secure than our current one.
The undefined behavior paradoxes were only added by the 90s, when optimizing compilers became a logic inference engine, feed with the unquestionable truth that the developer never exercises UB.
Just because it was a sane language for kernel development at the 1980s, it doesn't mean it is one now.
Yeah, because the only way to achieve performance in 1980's C on 16 bit platforms was to litter it with inline Assembly, thus UB based optimisation was born to win all those SPEC benchmarks in computer magazines.
Funny thing in that the same thing stopping people to write those crazy¹ optimizers at the 1980s was exactly the lack of capacity of the computers to run them.
What means that they appeared exactly at the time the need for them became niche. And yet everybody adopted them due to the strong voodoo bias we all have at computer-related tasks.
1 - They are crazy. They believe the code has no UB even though they can prove it has.
A great quote. She's absolutely right. C has absolutely polluted people's understanding of what compiler optimizations should be. Compilers should make a program go faster, invisibly. C makes optimization everyone's problem because the language is so absolutely terrible at defining its own semantics and catching program errors.
The undefined behavior I struggle with keeps me from better performance though. I have something like [(uint32_t value) >> (32 - nbits)] & (lowest nbits set). For the case of nbits=0, I would expect it to always return 0, even if the right shift of a 32-bit value by 32 bits is undefined behavior, then bit-wise and with 0 should make it always result in 0. But I cannot leave it that way because the compiler thinks that undefined behavior may not happen and might optimize out everything.
Exactly. The irony in all of this is that C is not a portable assembler. It'd be better if it were[1]!
If you want the exact semantics of a hardware instruction, you cannot get it, because the compiler reasons with C's abstract machine that assumes your program doesn't have undefined behavior, like signed wraparound, when in some situations you in fact do want signed wraparound, since that's what literally every modern CPU does.
[1] If the standard said that "the integer addition operator maps to the XYZ instruction on this target", that'd be something! But then compilers would have to reason about machine-level semantics to make optimizations. In reality, C's spec is designed by compiler writers for compiler writers, not for programs, and not for hardware.
I think that the undefinde behaviour should be partially specified. In the case you describe, it should require that it must do one of the following:
1. Return any 32-bit answer for the right shift. (The final result will be zero due to the bitwise AND, though, regardless of the intermediate answer.) The intermediate answer must be "frozen" so that if it is assigned to a variable and then used multiple times without writing to that variable again then you will get the same answer each time.
2. Result in a run-time error when that code is reached.
3. Result in a compile-time error (only valid if the compiler can determine for sure that the program would run with a shift amount out of range, e.g. if the shift amount is a constant).
4. Have a behaviour which depends on the underlying instruction set (whatever the right shift instruction does in that instruction set when given a shift amount which is out of range), if it is defined. (A compiler switch may be provided to switch between this and other behaviours.) In this case, if optimization is enabled then there may be some strange cases with some instruction sets where the optimizer makes an assumption which is not valid, but bad assumptions such as this should be reduced if possible and reasonable to do so.
In all cases, a compiler warning may be given (if enabled and detected by the compiler), in addition to the effects above.
I wanted to reply that your point 3 should already be possible with C++ constexpr functions because it doesn't allow undefined behavior. But I it seems I was wrong about that or maybe I'm doing it wrong:
The first output will print a random number, 140728069214376 in my case, while the second line will always print 1. However, when I put the ( ( 1ULL << nBits ) - 1U ) part into a separate function and print the values for that, then getBits( 0 ) suddenly always returns 0 as if the compiler understands suddenly that it will and with 0.
In this case, the compiler will only print a warning when trying to call it with getBits2<0>. And here I kinda thought that constexpr would lead to errors on undefined behavior, partly because it always complains about uninitialized std::array local variables being an error. That seems inconsistent to me. Well, I guess that's what -Werror is for ...
Unfortunately constexpr doesn't imply constant evaluation. Your function can still potentially be executed at runtime.
If you use the result in an expression that requires a constant (an array bound, a non-type template parameter, a static_assert, or, in c++20, to initialize a constinit variable), then that will force constant evaluation and you'll see the error.
Having said that, compilers have bugs (or simply not fully implemented features), so it is certainly possible that both GCC and clang will fail to correctly catch constant time evaluation UB in some circumstances.
Eh. The existence of Rust (and Zig, to a lesser extent) prove that you can, in fact, have both: Highest performance and safe, properly error checked code without any sort of UB.
UB is used for performance optimizations, yes, but all of these difficult to diagnose UB issues and bugs happen because C++ makes it laughably easy to write incorrect code, and (as shown by Rust) this is by no means a requirement for fast code.
The Computer Language Benchmarks Game has C++ outperforming Rust by around 10% for most benchmarks. Binary trees is 13% faster in C++, and it's not the best C++ binary tree implementation I've seen. k-nucleotide is 32% faster in C++. Rust wins on a few benchmarks like regex-redux, which is a pointless benchmark as they're both just benchmarking the PCRE2 C library, so it's really a C benchmark.
> because C++ makes it laughably easy to write incorrect code
I was going to ask how much you actually program in C++, but I found a past comment of yours:
> I frankly don't understand C++ well enough to fully judge about all of this
> Rust wins on a few benchmarks like regex-redux, which is a pointless benchmark as they're both just benchmarking the PCRE2 C library, so it's really a C benchmark.
The Rust #1 through #6 entries use the regex crate, which is pure-Rust. Rust #7[rust7] (which is not shown in the main table or in the summary, only in the "unsafe" table[detail]) uses PCRE2, and it is interestingly also faster than the C impl that uses PCRE2[c-regex] as well (by a tiny amount). C++ #6[cpp6], which appears ahead of Rust #6 in the summary table (but isn't shown in the comparison page)[comp], also uses PCRE2 and is closer to Rust #7.
I mean, it's outperforming C as well in that particular benchmark.
Lies, damn lies, and benchmarks?
I can at least say, the performance difference between C, C++, and Rust, is splitting hairs.
If you want to write something performant, low level, with predictable timing, all three will work.
I'm spending a lot of time building projects with Rust & C++ these days. The issue/tradoff isn't performance with C++, but that C++ is better for writing unsafe code than Rust.
> C++ makes it laughably easy to write incorrect code
It also provides a lot of mechanisms and tools to produce correct safe code, especially modern C++. Most codebases you're not seeing a lot of pointer arithmetic or void pointer or anything of that nature. You hardly even see raw pointers anymore, instead a unique_ptr or a shared_ptr. So yes, you can write incorrect code because it's an explicit design goal of C++ not to treat you like a baby, but that doesn't mean that writing C++ is inherently like building a house of cards.*
You can do any rust optimization yourself in C++ (ie. aliasing assumptions), whereas rust makes the other way around very difficult, often forcing you to use multiple layers of indirection where c++ would allow a raw pointer, or forcing an unwrap on something you know is infallible when exceptions would add no overhead, etc. Rust programmers want people to believe that whatever appeases the supposedly zero cost borrow checker is the fastest thing to do even though it has proven to be wrong time and time again. I can’t tell you how many times I’ve seen r/rust pull the “well why do you want to do that” or “are you sure it even matters” card every time rust doesn’t allow you to write optimized code.
> You can do any rust optimization yourself in C++ (ie. aliasing assumptions)
I don’t think that is entirely true. C++ doesn’t have any aliasing requirements around pointers, so if the compiler sees two pointers it has to assume they might alias (unless the block is so simple it can determine aliasing itself, which is usually not the case), but in Rust mutable references are guaranteed to not alias.
This was part of the reason it took so long to land the “noalias” LLVM attribute in Rust. That optimization was rarely used in C/C++ land so it had not been battle tested. Rust found a host of LLVM bugs because it enables the optimization everywhere.
While standard C++ has no equivalent of a noalias annotation, it's wrong to say that it has no aliasing requirements. To access an object behind a pointer (or a glvalue in general), the type of the pointer must be (with a few exceptions) similar to the type of the pointee in memory, which is generally the object previously initialized at that pointer's address. This enables type-based alias analysis (TBAA) in the compiler, where if a pointer is accessed as one type, and another pointer is accessed as a dissimilar type, then the compiler can assume that the pointers don't alias.
Meanwhile, Rust ditches TBAA entirely, retaining only initialization state and pointer provenance in its memory model. It uses its noalias-based model to make up for the lack of type-based rules. I'd say that this is the right call from the user's standpoint, but it can definitely be seen as a tradeoff rather than an unqualified gain.
Because existing unsafe code written in stable Rust depends on the ability to convert raw pointers and references from one type to another, as long as their memory layouts match. That's the whole premise of the bytemuck crate [0], and it's the basis for things like the &str-to-&[u8] or &[u8]-to-&str conversions in the standard library.
That is correct. However, raw pointers are not borrow checked, in safe Rust they're largely useless, but in unsafe Rust you can use raw pointers if that's what you need to do to get stuff done.
As an example inside a String is just a Vec<u8> and inside the Vec<u8> is a RawVec<u8> and that is just a pointer, either to nothing in particular or to the bytes inside the String if the String has allocated space for one or more bytes - plus a size and a capacity.
The argument for the Exception price is that we told you Exceptions were for Exceptional situations. This argument feels reasonable until you see it in context as a library author.
Suppose I'm writing a Clown Redemption library. It's possible to Dingle a Clown during redemption, but if the Clown has already dingled that's a problem so... should I raise an exception? Alice thinks obviously I should raise an exception for that, she uses a lot of Clowns, the Clown Redemption library helps her deliver high quality Clown software and it's very fast, she has never dingled a Clown and she never plans to, the use of exceptions suits Alice well because she can completely ignore the problem.
Unfortunately Bob's software handles primarily dingling Clowns, for Bob it's unacceptable to eat an exception every single damn time one of the Clowns has already been dingled, he demands an API in which there's just a return value from the dingling function which tells you if this clown was already dingled, so he can handle that appropriately - an exception is not OK because it's expensive.
Alice and Bob disagree about how "exceptional" the situation is, and I'm caught in the middle, but I have to choose whether to use exceptions. I can't possibly win here.
Like I said, this argument doesn’t work because you can use options in c++ but you can’t use exceptions in rust. So when there’s an occasion where you want to avoid the overhead of an option or result in rust - well too bad.
If all you care about is outright performance, having the option for exceptions is easily the superior choice. The binary does get bigger but those are cold pages so who cares (since exceptions are exceptional, right?)
Expanding this for those not familiar with Go's history: Ken Thompson, formerly of Bell Labs and co-creator of B and Unix, was deeply involved in Go's early days. Rob Pike, also ex-Bell Labs, was also one of Go's principal designers.
I can't find a source now, but I believe Rob described Russ Cox as "the only programmer I've met as gifted as Ken." High praise indeed.
Go it's modelled after plan9'C [1-9]c = go cross compiling, and Limbo so it has a lot of sense. Both come from the same people after all. Unix->Unix8 -> Plan9 -> Go.
I suspect that there is some huge number of developer hours that have been wasted, and huge amount of money wasted, on cleaning up after security breaches and finding and fixing security issues. I suspect that those numbers dwarf any losses that might have arisen due to reduced developer productivity or reduced performance when using a (hypothetical) C-like language that doesn't allow the compiler to do these sorts of things.
"a (hypothetical) C-like language that doesn't allow the compiler to do these sorts of things."
It's not very hypothetical in 2023. There are plenty of languages whose compilers don't do this sort of thing and attain C-like performance. There isn't necessarily a single language that exactly and precisely replaces C right now, but for any given task where you would reach for C or C++ there's a viable choice, and one likely to be better in significant ways.
I also feel like this is missed by some people who defend this or that particularly treatment of a particular undefined behavior. Yeah, sure, I concede that given the history of where C is and how it got there and in your particular case it may make sense. But the thing is, my entire point is we shouldn't be here in the first place. I don't care about why your way of tapping a cactus for water is justifiable after all if you consider the full desert context you're living in, I moved out of the desert a long time ago. Stop using C. To a perhaps lesser but still real extent, stop using C++. Whenever you can. They're not the best option for very many tasks anymore, and if we discount "well my codebase is already in that language and in the real world I need to take switching costs into account" they may already not be the best option for anything anymore.
I assume you're referring to "my codebase is already in C/C++"? Which I did acknowledge?
Because otherwise, what the world is increasingly not aligning with is using C when you shouldn't be. Security isn't getting any less important and C isn't getting any better at it.
This reasoning is why software keeps getting slower and more bloated, build times increase, and latency goes up despite having orders of magnitude more compute power.
If whatever language you're thinking of does that, it isn't one of the ones I'm talking about. I sure as heck aren't talking about Python here. Think Rust, D, Nim, in general the things floating along at the top of the benchmarks (that's not a complete list either).
I don't see how this solves anything, Nim's backend is C, which means it should suffer from the same pitfalls. They probably clean it up and eliminate UB, but it should still exist.
Yeah, but with less bugs, more features, and faster development. I mean I hate Electron with a passion but it means everybody gets a client. Let’s not pretend that it’s all worse rather than a set of tradeoffs.
You don't need one faster. You need one as fast. These options generally exist. Rust seems to have crept its way right up to "fast as C"; it isn't really a distinct event, but https://benchmarksgame-team.pages.debian.net/benchmarksgame/... (it tends to better on the lower ones so scroll a bit). There are some other more exotic options.
C isn't the undisputed speed king any more. It hasn't necessarily been "roundly trounced", there isn't enough slack in its performance for that most likely, but it is not the undisputed speed king. It turns out the corners it cuts are just corners being cut; they aren't actually necessary for performance, and in some cases they can actually inhibit performance. See the well-known aliasing issues with C optimizations for an example. In general I expect Rust performance advantages to actually get larger as the programs scale up in size and Rust affords a style that involves less copying just to be safe; benchmarks may actually undersell Rust's advantages in real code on that front. I actually wouldn't be surprised that Rust is in practice a straight-up faster language than C on non-trivial code bases being developed in normal ways; it is unfortunately a very hard assertion to test because pretty much by definition I'm talking about things much larger than a "benchmark".
Yes, but then complier vendors started abusing UB to increase performance and while silently decreasing safety/correctness. If the compiler creates a security bug by optimizing away a bounds check the programmer explicitly put there, that's a problem.
It appears the author is a Go advocate. I assume they are valuing clearly defined error checking/handling/reporting (the authors definition of correctness) over performance. If that's what you are looking for, consider Go.