8 ms·
I think the real problem stems from the mismatch between modern processors and the processors C was originally designed for. C programmers want their code to b
by ChrisSD 5y ago
I think the real problem stems from the mismatch between modern processors and the processors C was originally designed for.
C programmers want their code to be fast. Vanilla C no longer gives them the tools to do that on a modern processor. Either the language needs to be extended or the compiler needs to get more creative in interpreting the existing language. The latter is the least disruptive and it doesn't stop judicious use of the former.
So, in short, UB is what gives room for the compiler to be more creative without the programmer having to change their code. It wasn't a reading error, it was an opportunity the compiler devs enthusiastically embraced.
- rectang 5y agoWhen compiler writers have get "creative" with C undefined behavior, programming C no longer produces predictable results. > least disruptive Like starting to optimize away loop checks that can "never happen" because signed integer overflow is UB, suddenly changing the behavior of programs that were fine for years? I wish I could just fence off this insanity by never starting another project in C. Unfortunately, C is ubiquitous in the ecosystem so all of us are stuck cleaning it up.
- msbarnett 5y ago> Like starting to optimize away loop checks that can "never happen" because signed integer overflow is UB, suddenly changing the behavior of programs that were fine for years? Yeah. Not doing that on modern processors is actually quite disruptive. Here: for(i = offset; i < (offset + 16); i++) { arr[i] = i + 32; } What C compilers currently do is, in line with the standard, ignore the case that offset + 16 might overflow. This makes this eligible for loop unrolling, and depending on the specifics of the math inside the loop, the compiler can do a lot to pre-calculate things because it knows this is happening 16 times exactly. If, instead, we force compilers to think about the fact that offset + 16 could have some implementation-defined meaning like wrapping, then all bets are off & we have to throw a bunch of optimization opportunities out the window. Lots and lots of hot & tight loops which are currently able to be compiled into something suitable for the preferences of modern CPUs instead has to be to be naively compiled because of the need to hold back due to the possibility of something that largely wasn’t happening, happening. Most people write most loops this way, never expecting or intending to overflow anything. Most loops are benefitting from this optimization. A lot of code would get slower, and programmers would have to do a lot more fragile hand-unrolling of operations to get that performance back. And they’d need to update that more often, as whatever the optimal “stride” of unrolling changes with the evolution of CPU pipelines. It’s slower code and more work more often for more people, to satisfy a minority use-case that should really just have its own separate “please wrap this” construct.
- rectang 5y ago> to satisfy a minority use-case Every single C program is potentially in that "minority". Nobody can tell when the compiler writers are going to change up behavior on you. It doesn't matter how carefully the codebase has been written, whether you've had `-Wall -Wextra` enabled. What was fine at one time is no longer fine today. Any C program may suddenly start exhibiting misbehavior from innocuous to catastrophic to horrendously insecure. It's psycho, maddening, irresponsible. And the only way to deal with it is to purge C programs compiled by these psychotic compilers from our systems.
- msbarnett 5y ago> Every single C program is potentially in that "minority". Nobody can tell when the compiler writers are going to change up behavior on you. This is ridiculously hyperbolic, and bringing unthinking emotional responses like "psycho" and "irresponsible" only obscures the fact that there are very serious engineering tradeoffs involved in trying to balance "not surprising people whose code contains an assumption that some case is going to behave a certain way when by-the-standard-as-written that case can be ignored" and "not making everything with a hot loop 8x slower because we can't assume anything about loop bounds any more", and that compilers that do the latter are unlikely to prove popular with a lot of people either.
- adwn 5y ago> If, instead, we force compilers to think about the fact that offset + 16 could have some implementation-defined meaning like wrapping, then all bets are off & we have to throw a bunch of optimization opportunities out the window. Uh huh. If `i` is declared as `unsigned int` instead of `int`, then overflow is defined and the compiler can't apply those optimizations. And yet the world doesn't end and the sun will still rise tomorrow...
- tom_mellior 5y agoThe world doesn't end, but in the "int" case you get nice vector code and in the "unsigned int" case you get much less nice scalar code: https://gcc.godbolt.org/z/cje6naYP4 https://gcc.godbolt.org/z/cje6naYP4
- tomcam 5y ago> Vanilla C no longer gives them the tools to do that on a modern processor Can you elaborate on this point?
- I_Byte 5y agoC was created during a time where instructions were executed linearly with no vectorization, memory was a flat space with no CPU caches, and there wasn’t a branch predictor that may or may not execute the correct program branch in advance. The list goes on but the rest is beyond my scope. C was designed for a now obsolete computer architecture model and over the years this old model has essentially become an abstraction that sits between C and the CPU. As such, C programmers aren’t really programming in their CPU’s domain anymore and C, by default, lacks the commands necessary to effectively utilize these new developments. It is left up to the compiler to translate C code from the old architecture abstraction into efficient machine code for our new machines. For a more in depth look into this topic I recommend you check out (0). (0) - https://queue.acm.org/detail.cfm?id=3212479 https://queue.acm.org/detail.cfm?id=3212479
- tomcam 5y agoThanks for the link. Excellent article. Those are all great points and I like having them summarized in one place, because I am indeed a little behind in my modern architecture theory. However it has always been acknowledged that C was by definition a sort of simplified computing model. For example, when I first learned see the 8086 architecture was popular but it was competing with many others and it was already dramatically different from the PDP-11 virtual machine you describe. The 286, 386 and so on had funky indexing modes and address space weirdness but so did just about every other processor of the time. There is likely never to be a single unified architecture that anyone agrees on, and the developers of C understood this, certainly by the time the 1989 standard was hammered out. So compiler directives, pragmas, and maybe even language extensions were expected on a per CPU basis, no?
- pjmlp 5y agoC originated as an evolution from BCPL, with B a stopgap, as means to rewrite UNIX. Thing is, BCPL main goal was an interim solution to Bootstrap CPL, nothing more than that. https://en.wikipedia.org/wiki/CPL_(programming_language) https://en.wikipedia.org/wiki/CPL_(programming_language) Unfortunely, UNIX's success means we got stuck with something that shouldn't be more than a portable macro assembler.
- bhk 5y agoEven on modern processors, an ADD instruction does not corrupt memory. The C standard, in declaring that an integer overflow results in all-bets-are-off UB, is not enabling compilers to provide valuable optimizations.
- nullc 5y agoIt would be nice if that were true, but it's not. The ability to assume incrementing a counter won't overflow makes range analysis possible in many cases where it otherwise wouldn't be. Because of this you can perform vectorization because the compiler can assume it knows how many times the loop will run. The performance differences are not small. You can also tell some compilers to treat signed integer overflow as wrapping-- but people don't usually do this because the optimizations you lose are valuable.
- vyodaiken 5y agoI bet the performance differences are minimal except perhaps on some contrived cases. The compiler should not assume things it doesn't know. But show me a benchmark.
- saagarjha 5y agoThere is no guarantee that an addition in C is even going to convert into an add instruction, though. Actually, on x86 it's more likely to turn into a lea because this optimizes port usage better.