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.
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
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.
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 bit of an understatement. The author is the current Golang project lead and a member since it’s inception
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.
[[nodiscard]] constexpr uint64_t
getBits( uint8_t nBits )
{
return BITBUFFER >> ( 64 - nBits ) & ( ( 1ULL << nBits ) - 1U );
}
int main()
{
std::cerr << getBits( 0 ) << "\n";
std::cerr << getBits( 1 ) << "\n";
return 0;
}
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. template<uint8_t nBits>
[[nodiscard]] constexpr uint64_t
getBits2()
{
return BITBUFFER >> ( 64 - nBits ) & ( ( 1ULL << nBits ) - 1U );
}
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 ...Compiled with -std=c++17 and clang 16.0.0 on godbolt: https://godbolt.org/z/qxxWW93Tx
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.
To add to your list, using C++20 consteval instead of constexpr also triggers the error.
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.
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.
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.
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.
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.
Isn't the overhead for C++ exceptions quite significant, especially if an exception is thrown?
Exception handling can also increase the size of the binary because of the additional data needed to handle stack unwinding and exception dispatch.
I think a number of optimizations are made quite a bit more complex by exception handling as well.
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.
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?)
> 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
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.
https://www.p99conf.io/2022/09/07/uninitialized-memory-unsaf...
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.
[comp]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[detail]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[rust7]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[c-regex]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[cpp6]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
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.*
Go really is in many ways more similar to early C in spirit than modern C is.
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.
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.
The point of undefined behavior is "if we specify this, we cause problems for some of the compile targets for this language".
https://thephd.dev/c-undefined-behavior-and-the-sledgehammer...
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.
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.
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".
res, err := InterceptNuke(enemyNuke)
if err != nil {
fmt.Println("NUCLEAR LAUNCH DETECTED!")
log.Fatal(err)
} else {
fmt.Printf("Phew, we're safe. Nuke intercepted after %d seconds.\n", res)
}
Very clearly defined errors indeed.