7 ms·
> C has undefined behaviour in the first place (unlike, say, Java) is to maximally enable compiler optimisation. My understanding is that C has undefined behav
by jstimpfle 8y ago
> C has undefined behaviour in the first place (unlike, say, Java) is to maximally enable compiler optimisation.
My understanding is that C has undefined behaviour where there is no universally sensible implementation across all architectures.
> Compiler optimisation generally means doing strange and unexpected things
In the first place it should make code faster. I'm no one to judge, but the general reception seems to be that exploiting undefined behaviour (by assuming it will never happen) rarely brings obvious speed advantages. "Strange and unexpected things" can be a bad tradeoff for a tiny speed advantage.
For example, I'll boldly state that optimizing out a "if (p == NULL)" only because p was dereferenced earlier never makes sense. Code containing such a check is never performance sensitive enough to justify the tradeoff (or it's really bad code).
And without being a compiler engineer I assume that it should be easy to change compilers to NOT do this optimization (I assume the not-NULL inference is a special case in the code that can simply be removed).
> In other words, C values performance over safety, and that's part of the point.
Many people value C because of control and allowing one to not paint oneself in a corner. Performance is only a side effect.
- chrisseaton 8y ago> I assume the not-NULL inference is a special case in the code that can simply be removed No I would think it happens naturally as a result of more generally giving abstract values stamps, narrowing the stamps based on expressions, and then simplifying expressions using those stamps. I would guess you wouldn't want stamps and optimisations based on them removed entirely.
- oddity 8y ago> > C has undefined behaviour in the first place (unlike, say, Java) is to maximally enable compiler optimisation. > My understanding is that C has undefined behaviour where there is no universally sensible implementation across all architectures. These are two ways of looking at the same thing. A naive translation of C statements to what seems like the most straightforward way to do them on some target architecture is an optimization over emulating some other architecture’s behavior. Consider the problem of translating one arch’s assembly to another’s. Essentially, C needs to be defined at a certain level of abstraction for all of this to work, but it also happens that detecting holes in this abstraction is undecideable.
- jstimpfle 8y agoThat's OT. There isn't any confusion what "optimization" means here.
- MaxBarraclough 8y agoPerhaps I'm being pedantic, but that's not really what 'optimisation' means. Optimisation refers to code transformations which result in output code which has better performance (whether in terms of space or time). Changing the definition of the source language, isn't optimisation. Whether the source language can cleanly be mapped to the target machine, may be significant to performance, but it's not optimisation.
- mpweiher 8y ago> in output code which has better performance Without changing the output. That's the crucial part of the contract current compilers are breaking. And then try to lawyer it away by claiming that the original was undefined anyway so they can do whatever the hell they want.
- iainmerrick 8y agoI'm no one to judge, but my impression is that the general reception is that exploiting undefined behaviour (by assuming it will never happen) rarely brings speed advantages. I’ll play devil’s advocate here, even though I strongly dislike the current direction of the major C compilers. This aggressive interpretation of undefined behavior apparently can enable some important optimizations. Here are a couple of examples: First, say you have an index variable where you increment it (i += n) and check it’s within bounds (i >= start && I < end). It could be useful to assume that i won’t overflow and wrap, so some of the bounds checks can be optimized out. (The downside is that if you want to explicitly check for overflow, it’s tricky to do so correctly.) Second, say you have a function that takes several pointers and copies data between them. It can be useful to assume those pointers aren’t aliased, so we can assume that memory isn’t changing in unexpected ways, and therefore it’s safe to optimize out some repeated reads. I do feel like those cases could be handled in different ways, though. In the second case, perhaps the compiler could insert a runtime check for aliasing, and branch to either a fast path or a slow path as appropriate. I’m also not sure that C really needs to try to be the fastest and most highly optimized language, even though it’s widely regarded as being very fast. As far as I know C isn’t the most popular language for scientific supercomputing, for example, where the users really do care about performance.
- userbinator 8y agoIt could be useful to assume that i won’t overflow and wrap, so some of the bounds checks can be optimized out. This is a very common example, but how hard is it for the compiler to prove that it won't overflow? I know in the general case it's the halting problem, but for specific cases (especially where a human inspecting the code would easily see) it should be easy --- especially those cases which warrant optimisation.
- jstimpfle 8y agoYou could go over your source code and see how many operations are done using non-constant data from non-local-scope sources. I would be pretty amazed by a compiler that could find all or most of my operations that don't overflow. Almost all never do, or shouldn't, but that's probably hard to prove and requires global analysis.
- MaxBarraclough 8y ago> C has undefined behaviour where there is no universally sensible implementation across all architectures. But that's really the same thing. Java takes the opposite approach: it lays down the law, and if your CPU does things differently, the JIT compiler will have to generate however many extra instructions are necessary to bend things to Java's approach. For instance, Java mandates 2's complement, where C does not. Java mandates the size of an 'int', where C does not. Java specifies exactly what happens if you attempt to dereference null, where C does not. (Too late I see that oddity just made the same points.) > In the first place it should make code faster Well of course, or it's not optimisation, it's obfuscation. > the general reception seems to be that exploiting undefined behaviour (by assuming it will never happen) rarely brings obvious speed advantages Depends what you mean by 'obvious' I suppose. For instance, Java pays a non-zero performance penalty for insisting on range-checks for array-element accesses. > I'll boldly state that optimizing out a "if (p == NULL)" only because p was dereferenced earlier never makes sense. No need for boldness: Apple says that UBSan's performance penalty is around 20%. https://developer.apple.com/documentation/code_diagnostics/undefined_behavior_sanitizer https://developer.apple.com/documentation/code_diagnostics/u... > Code containing such a check is never performance sensitive enough to justify the tradeoff C programmers tend to hold some disdain for the attitude that they should be made to trade off performance for safety, and not without reason. Claiming they should 'never' care about the overhead of some check, isn't the C way. If you want a safe language, don't use C. (Well, there's MISRA C, I suppose.) > it should be easy to change compilers to NOT do this optimization I don't know exactly what change you're proposing. If you manually check for null, the compiler can't just eliminate your check. Or do you mean C should always check for null at every dereference? The overhead there would be considerable. > Performance is only a side effect No, performance is central to the appeal of C.
- nerdponx 8y agoI don't know exactly what change you're proposing. If you manually check for null, the compiler can't just eliminate your check. Their post was predicated on the assumption that the compiler can in fact do this if 'p' was dereferenced before the check. Is that not the case?
- Sharlin 8y ago> My understanding is that C has undefined behaviour where there is no universally sensible implementation across all architectures. There are two less drastic options used by the Standard for those cases. - Unspecified behavior: behavior that must be "sane" in some sense but may differ between implementations. Vendors need not document the exact behavior. - Implementation-defined behavior: behavior that must be sane and may differ between implementations. Vendors are required to document the behavior.
- dbaupp 8y ago> My understanding is that C has undefined behaviour where there is no universally sensible implementation across all architectures. That's somewhat true for things like signed integer overflow, but it's more than that. For instance, if there wasn't UB around pointers being "in bounds", then either there would have to be expensive checking of every pointer dereference ever (both read and write), or other performance drains like values never ending up in registers (both literally and conceptually) for extended periods, they'd have to be loaded from memory every time they're used, effectively treated as `volatile`. (And, this inhibits other optimizations like breaking up structs via SROA.) For instance, int f(int *p) { int x = 0; *p = 10; if (x != 0) { ... } } The compiler can't optimize out the branch unless it can assume (or dynamically ensure) that the dereference of the pointer p won't point to an unrelated object (x). Arbitrary transformations on pointers (including constructing them directly from integers, and one might guess the stack address of x) are "only" undefined behaviour in C, not prohibited statically.
- MaxBarraclough 8y ago> Arbitrary transformations on pointers (including constructing them directly from integers, and one might guess the stack address of x) are "only" undefined behaviour in C, not prohibited statically. Sure, that's what it means to have full pointer arithmetic. It's very much an aim away from face feature of the language.
- dbaupp 8y agoYes, that is exactly my point. It's a dangerous feature of the language, or, said another way, has a lot of potential for undefined behaviour. If one is trying to reword the standard to control undefined behaviour, this is one feature that needs to be considered.
- caf 8y agoFor example, I'll boldly state that optimizing out a "if (p == NULL)" only because p was dereferenced earlier never makes sense. Code containing such a check is never performance sensitive enough to justify the tradeoff (or it's really bad code). This kind of thing really comes to the fore when the compiler is doing a lot of inlining. Say you have some kind of utility function: int frob(struct foo *f) { int r; if (f != NULL) { /* things */ } else { /* other things */ } /* more things */ return r; } ..and you call this function all over the place. If some of those callsites end up with `frob()` being inlined, and the compiler can see that the pointer passed is never NULL at that particular site, it can usefully eliminate the dead code. This despite the fact that the code isn't dead in general - just at some of the callers.
- jstimpfle 8y agoOn the other hand, if the chunk is significant enough to reward removal, the function probably shouldn't be inlined in the first place...
- AstralStorm 8y agoThen maybe it should, cause you're using only happy hot path and not the NULL one. Removing even one branch and debloating cache in a hot path can be significant. And then DCE will enable further optimizations. If you want to ensure something is not inlined, you're free to do so.
- jstimpfle 8y agoIf it were really that performance critical the programmer would not call a swiss army knife utility function that contains a lot of unneeded code (replace "would" by "should" if you like). If compiler optimization gets to be more and more about making poorly structured code barely faster, no wonder we have all these discussions about uncalled-for optimizations.
- AstralStorm 8y ago
- jstimpfle 8y ago> was dereferenced earlier The example I should have given was "is dereferenced LATER". Because only in this setting the optimization actually has an effect on most platforms (where dereferencing NULL segfaults).
- caf 8y agoThe optimisation isn't valid unless the dereference always happens. The criticism arose because this optimisation had been applied in a kernel environment, where the dereference happened before the test, but in some circumstances might not trap. It isn't something that concerns normal applications, though.
- jstimpfle 8y agoI should have researched a better example. But anyways, what about a case like this one? int *p = get_the_pointer(); if (p == NULL) { remove_me(); } printf("Hello %d\n", *p); At least for some things that remove_me() could do, wouldn't it be possible for a compiler to remove the branch entirely? I mean I'm pretty sure that compilers can move the conditional below the printf under certain circumstances (as long as there isn't an observable difference for C's definition of "observable"). Add to that the UB and the compiler might as well remove the conditional branch. (And this would be a case that can really take programmers by surprise when debugging).
- caf 8y agoYes, it depends on whether remove_me() (provably) always returns or not. Optimisations frequently do things that take people by surprise when debugging, but that's par for the course.
- MaxBarraclough 8y ago> Yes, it depends on whether remove_me() (provably) always returns or not. Oops, I hadn't thought of that. So the compiler isn't permitted to assume that a function-call will return? Surely that's a huge obstacle to optimisation.
- kazinator 8y agoC has undefined, unspecified and implementation-defined behavior in those places where there was no implementation consensus 35 years ago; it's just maintaining the status quo. The thing that were and were not codified as requirements in 1989 ANSI C were a historic accident of the time; now that historic accident is just being preserved, more or less. E.g. we must have unspecified evaluation order of function arguments because we've always had unspecified evaluation order of function arguments.
- MaxBarraclough 8y agoI'll ramble about the evaluation order thing: I presume the reason the C standards folks refuse to mandate left-to-right evaluation order is that it might cause trouble for some arcane embedded compiler somewhere, or something. The original intention was to enable optimisations, after all. If you do make_coffee( get_barista(), get_beans() ); then if your target platform uses a stack-based calling convention, you'll want to do those two 'inner' calls in a certain order (depending on whether it's a 'reversed' stack-based calling convention or not). If you're passing by registers, it will presumably make no difference. It's a pretty wacky micro-optimisation by anyone's standards, and allows strange platform-specific bugs to arise, but I presume the refusal to specify evaluation order is just the standards body exhibiting a healthy resistance to changing the very stable C language. They don't want to cause a downside for anyone, even the strangest half-baked compilers.
- kazinator 8y agoThey could specify that there are exactly two possible evaluation orders (left-right or right-left) the choice of which is implementation-defined (must be documented) and that there is a sequence point between the evaluations of the argument expressions. (As well as between evaluating the function designating expression and the arguments.) The fact that there are N! possible evaluation orders for N argument expressions: which implementation does that serve? Or that there is no sequence points: which calling convention does that serve?
- kazinator 8y agoIf the compiler has deduced statically that a p == NULL expression might be executed after a dereference of p, the number one priority is to issue a diagnostic for this situation, because it is quite likely a coding mistake. The programmer almost certainly didn't put in the dereference on purpose, as a way of encouraging p == NULL to be optimized away! It's very irresponsible to treat situations that are coding mistakes as "let's assume there is no mistake and just optimize".
- MaxBarraclough 8y ago> It's very irresponsible to treat situations that are coding mistakes as "let's assume there is no mistake and just optimize". Well, that's the C philosophy, and it's part of the reason it's so fast. As I've said repeatedly throughout this thread, if you want a safe language, why are you using C? If the compiler detects undefined behavior, it should of course issue a loud warning to the programmer, but that's not the issue. The issue is that the compiler assumes absence of UB and optimises accordingly, spending relatively little effort investigating whether UB is actually present. If that's what you want, use a proper static-analysis tool. (They really deserve more use!)
- kazinator 8y agoThe compiler has deduced that a comparison of a pointer to null makes no sense, because it was dereferenced; if that code is reached, the comparison will always be true. That should be diagnosed, just like, say, an unsigned-type > 0 comparison that is always true. > why are you using C? For detailed control of the machine with minimal run-time support, without resorting to assembly language. Basically, the original reasons for C, before all this historic revisionism. I want reasonable speed: good use of registers and basic optimizations like jump threading and peephole code improvements. Optimization in C is otherwise the job of the programmer. The machine code should more or less follow from the calculations that are written in the source, and every memory access through pointers should be there, in the order it appears. The "trust the programmer" mantra never meant "trust that there is no undefined behavior" but simply "trust that the programmer put every every expression in place for a reason and just translate it to machine code".