Hacker News new | ask | show | jobs
by pcwalton 1720 days ago
In general I've long been very skeptical of removing optimizations that rely on undefined behavior. People say "I'd happily sacrifice 1% for better theoretical semantics", but theoretical semantics don't pay the bills of compiler writers. Instead, compiler developers are employed by the largest companies, where a 1% win is massive amounts of dollars saved. Any complaint about undefined behavior in C must acknowledge the underlying economics to have relevance to the real world.

As the paper notes, there are plenty of alternative C compilers available to choose from. The reason why GCC and LLVM ended up attaining overwhelming market share is simply that they produce the fastest possible code, because, at the end of the day, that is what users want.

If you want to blame someone, blame the designers of the C language for doing things like making int the natural idiom to iterate over arrays even when size_t would be better. The fact that C programmers continue to write "for (int i = 0; i < n; i++)" to iterate over an array is why signed overflow is undefined, and it is absolutely a critical optimization in practice.

4 comments

The reason why GCC and LLVM ended up attaining overwhelming market is simply that they produce the fastest possible code

No, I think it's more because they are free.

In my experience, ICC can be much better at instruction selection while also not being so crazy with exploiting UB.

I'm also skeptical about the often-claimed superiority of ICC. The numbers I've seen are very equivocal.

Besides, it's irrelevant: there are lots of free C compilers that don't exploit UB, and also rarely get used.

Lots? None are usable to replace our current insane compilers, removing code without warnings.

None of pgcc, sdcc, tcc, lcc and friends work with current code bases, plus they have severe bugs.

ICC nowadays is based on LLVM (https://software.intel.com/content/www/us/en/develop/blogs/a...).

That might mean the differences have mostly disappeared, but that may depend on what the front end (icx vs clang) does.

They also support the widest range of CPU's
It is because they are "free" and also because they attract massive investment from FAANG and National Labs etc.
Uh, massive investment? The commit logs reveals how many people work on them, and while it's massive compared to single-pizza teams I've worked in buildings where all of them could get a desk each.
> If you want to blame someone, blame the designers of the C language for doing things like making int the natural idiom to iterate over arrays even when size_t would be better. The fact that C programmers continue to write "for (int i = 0; i < n; i++)" to iterate over an array is why signed overflow is undefined, and it is absolutely a critical optimization in practice.

Well, size_t is unsigned and has defined overflow, so you'd lose the optimization if you switched to it. (Specifically, there's cases where defining overflow means a loop is possibly infinite, which blocks all kinds of optimizations.)

Many languages try to fix this by defaulting to wrap on overflow, but that was a mistake because you rarely actually want that. A better solution is to have a loop iteration statement that doesn't have an explicit "int i" or "i++" written out.

The optimization I'm referring to is widening a 32-bit loop IV to 64-bit so it can stay in a register: https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...

size_t obviates the need for this optimization.

That's a strange example since it doesn't prove their point.

"int count; … for (int i = 0; i < count; ++i) {}" can't overflow so the optimization is always valid.

"int count; … for (int i = 0; i < count; i += 2) {}" is more of a problem.

I don't think "have a 64-bit int type" is the right approach for a new language either… we should be aiming for safety. If a variable's valid values are 0-50 then its type should be "integer between 0 and 50", not "integer between 0 and UINT64_MAX". Storage size should be an implementation detail.

ADA has this exact functionality: https://www.adaic.org/resources/add_content/standards/05aarm...

Look for the "range" keyword

There was no size_t in K&R1. Size_t was introduced in the standards process as was the definition of index variables in the for loop. You may have a complain with the standard there.

As for the optimization, it is based on misunderstanding of C semantics. The only place where the sign extend makes a difference is where pointers are longer than "ints" AND where the the iterator can overflow, and in that case, sign extend only makes a difference if the loop is incorrectly coded so that the end condition is never true. The code should just provoke a warning and then omit the sign extend (and it almost certainly doesn't make much of a difference since sign extend is highly optimized and has zero cost in a pipelined processor).

> There was no size_t in K&R1. Size_t was introduced in the standards process as was the definition of index variables in the for loop. You may have a complain with the standard there.

I certainly do. C and descendants makes you over-specify variables by declaring them all int/size_t/etc individually. It should've had C++11-style "auto" from the start and there should be a statement like "for i=0..n" that declares "i" the same type as "n".

We make a distinction between undefined and implementation defined behaviour for a reason. Saying that certain runtime behaviours result in malformed programs while being impractical to generate explicit checks for is not entirely crazy.

That said, I believe the set of undefined behaviours in our current standards is much, much too large - most of these should rightly be filed in the implementation-defined category instead. It is no longer the 70s and the very same modern compilers that perform more and more extreme optimisations year on year really do not need to account for a giant zoo of quirky experimental architectures; over the decades we've basically settled on a consensus on how pointers, integers, floating-point numbers etc ought to work.

Priorities change though; in particular, security is a rising priority. So it's not inevitable that funded compiler development must focus on wringing every last optimization out of software that might perform well enough if it were simpler. Organizations like NLnet and ISRG, for example, could fund work on getting to a usable Unix-like system that's entirely compiled with one of the simpler C compilers that don't exploit undefined behavior so much. They could justify it with the argument that security is best achieved through simplicity at all levels of the stack, including a simple build toolchain.