Hacker News new | ask | show | jobs
by AnimalMuppet 2945 days ago
You can care enough about efficiency to use C, and still not care about the last 0.1% of efficiency. You can especially not care about the last 0.1% of efficiency in 99% of your code.
2 comments

If you're compiling C with -O0 (as OP implies)... it's not just the last 0.1% of efficiency you're missing out on. Modern C compilers generate really, really crappy code at -O0, and you're looking at a 10x, 20x slowdown by forgoing optimizations. At those slowdowns, using an interpreted language that lacks a JIT starts to look competitive in performance.
Which is why it's so important to have -- ultra-clear -- (have you looked at the clang documentation or GCC manpages?) guidelines on how to disable optimizations that can be dangerous/unpredictable (strict-aliasing comes to mind) "within" -O1 or -O2 or -Os

One shouldn't need to throw the baby out with the bathwater (-O0) in order to get some semblance of semantics that won't pull the rug under your feet when you're not looking.

In practice, what these requests tend to boil down to is requests for the compiler to read the programmer's mind (strict aliasing is pretty much the biggest exception). What optimizations do you think are "dangerous/unpredictable"? Demanding that things like traps happen predictably means that you heavily constrain the ability to do dead-code elimination (can't eliminate code that could trap!) or loop-invariant code motion, two of the biggest performance wins, especially for things that could trap such as memory loads and stores, which are the code you most want to avoid whenever possible for performance.

Undefined behavior essentially says that compilers don't have to care about what happens in the cases that would constitute undefined behavior. This doesn't manifest in the compiler as if (undefined_behavior()) { destroy_users_code(); }, contrary to popular opinion. It instead tends to manifest as logic like "along this control-flow path, this condition is true, so we can now thread the jump from block A to block C since you're redundantly checking a known-true condition" and only after unwrapping several layers of computed assumptions do you find the "we assumed overflow cannot occur" at the bottom.

Exactly this.

There seems to be some idea that there is some "evil pass" in the compilers which looks for undefined behavior and then maliciously optimizes surrounding code to do something the programmer didn't expect.

Not at all. Even all the examples of UB leading to unexpected optimizations usually involve a chain of events, with very straightforward and necessary optimizations being involved, like value propagation, inlining, dead-code elimination, etc - and these aren't inherently related to "exploiting UB". You could (in some compilers) disable one of the things in the chain and perhaps avoid the problem for that example, but you'd also hurt a lot of other code that relied on the optimization.

That's one of the problems with people asking for specific small snippets of code where the UB-related transformation produced a big gain: you can certainly find these (but they'll often be picked apart) - but the bigger problem is not with the specific examples: it's that whatever optimization optimization you disable to make the small example work as you'd expect might then produce worse code across your application.

So people who want to disable the optimization that does "that" are often incorrectly assuming there is a small simple optimization which leads to "that" in the first place.

Still, I definitely agree that the situation regarding UB is depressing in many respects. Many of the decisions made by the C committee in the past haven't aged well. If you take a look at the low-level optimizations afforded by the largely-deterministic Java specification, they are largely at the same level as C - but Java had the benefit of coming along a couple of decades later where many open questions in C's time, such as integer overflow behavior, integer sizes, shift behavior, pointer models, etc, had largely been resolved. Platforms that don't conform to the JVM's model of an ideal machine will just have to generate slow code in some cases.

I'm not requesting the compiler to read my mind, I'm just asking for dead obvious and simple guidelines that allow me to perform a cost-benefit analysis and tune the compiler's behavior to what I consider acceptable.

Examples:

I don't care at all about optimizations that are a result of treating signed integer overflow as undefined behavior. I'll go for predictable, deterministic behavior every single time.

Same for strict aliasing rules. I find it absolutely insane and mind boggling that -O2 enables strict aliasing amongst who knows what else (50+ other flags). Why can't the impact of these optimization flags be easier to deduce? Why do I feel like I need to be a compiler developer just to get some measure of confidence in what the optimizers are doing? It's insane that there are people who treat this sort of unwarranted, dangerous complexity as a rite of passage and don't push for something that's better. Most importantly, it's terrifying that a significant chunk of C programmers _are not even aware of these issues_.

C will stick around for decades and will continue to be picked up by newcomers. It doesn't have to stay as dangerous and uncompromising as it is now.

EDIT: There is some progress with things like ubsan and asan but alas they're fairly limited platform-wise. What's worrying is that there hasn't been a clear shift in the mentality of those who control the language as the OP indicates in his report.

> I'm not requesting the compiler to read my mind, I'm just asking for dead obvious and simple guidelines that allow me to perform a cost-benefit analysis and tune the compiler's behavior to what I consider acceptable.

And how can I, as a compiler writer, know what you, the compiler user, consider acceptable without reading your mind?

It would be nice if we had some sort of contract. I, the compiler writer, will say that, so long as you write code that conforms to this contract, I will faithfully execute that code. And you, the programmer, will say that you will only write code that conforms to this contract, and that I don't need to worry about you writing code that fails to conform to this contract. Well, somebody already wrote this contract: it's the C specification.

What you're really saying is that you don't like the contract you have, which is a totally valid position to take (I myself would love to see strict aliasing go die in a fire). But almost no one who objects to the C contract is willing to actually negotiate a new contract. Instead, it's almost invariably a complaint that amounts to "you fucking compiler writers are ruining my code with your stupid optimizations." Well, no, your code was already broken per the contract; we were just too dumb to realize it earlier. And for the big undefined behaviors where well-behaved semantics are feasible (such as strict aliasing or signed integer overflow), we provide options to let you say "I don't agree to the C contract, I want to agree to a not-quite-C contract instead."

Many people right now don't realize that undefined behavior is merely a contract that the programmers not to create these statements, true. But that's why we, as compiler writers, try to educate people about why undefined behavior exists, what it means for programmers, and providing tools to make it much easier for programmers to find where it exists (hence things like asan and ubsan).

> C will stick around for decades and will continue to be picked up by newcomers. It doesn't have to stay as dangerous and uncompromising as it is now.

Exactly, even if most of us don't touch it directly, we need to use systems that make an heavy use of C (including Objective-C and C++), so it would already be an improvement to our computing stack if C could be improved as such.

Every once in a while I have to compile a kernel without optimizations. The performance penalty you pay is far away from 0.1%, it is definitely very noticeable on first glance. There might be projects where even that does not matter, but I would not think those are the majority.
An order of magnitude slower on non-trivial computation-heavy code (i.e,. not just doing a lot of IO or calls into libraries/the kernel) is a pretty good rule of thumb. Sometimes better, sometimes much worse.

The more layers of abstraction, the worse the penalty for not optimizing, so C++ is usually more heavily affected than C, for example: many of the so called "zero-cost" abstractions in C++ rely on a good optimizer.