News for hackers. Stuff that matters.
Re:The Dangers of Super Smart Compilers by nkurz on Sunday December 20, 2015 @02:24PM (#10767960)
This is a good example, and I think shows persuasively that it's good for a compiler to make logical assumptions about inputs when inlining code. But I think this is different than making optimizations by presuming the absence of undefined behavior when compiling the function itself.

Consider a programmer who feels it's clearer to dereference a and b once, rather than writing using * a and * b in the complicated math after the preliminary checks. And because they don't want to rely on C99 or distract by opening a new block, they put the conversion at the top of the function. Oh, and the new guy decided to add the "missile launch" code here.

    int computation(int *ptr_a, int *ptr_b) {
        int val_a = *a, val_b = *b;
        if(ptr_a == NULL && ptr_b == NULL) return 0;
        if(ptr_a == NULL) return val_b;
        if(ptr_b == NULL) return val_a;
        launch_missiles(); /* OK to put this here? */
        return ...something with val_a and val_b...;
    }

I think there is a reasonable argument that there is a difference between optimizing when a code path can be shown to never occur because of constrained inputs, and optimizing because behavior supposedly can be inferred from the absence of undefined behavior.

I'm fine with a literalist compiler that produces code that crashes when it immediately tries to dereference a NULL pointer, with one that produces code that does the safety checks but defers the load, but even if allowed by the spec[1] I don't think it's good behavior for a compiler to silently omit the safety checks and launch the missiles when passed a NULL.

[1] Is this undefined behavior according to the spec? I consider myself an expert C programmer, and I think it's undefined in C99, but I wouldn't bet that I've correctly understood the issues for the other dialects of C, muchless C++. Is a conforming compiler "allowed" to skip the safety checks and launch the missiles? I don't know, and thus defensively wouldn't write the code like this, but it would be nice to think that long-standing mission-critical code written by others won't suddenly stop working with a new release of the optimizing compiler.

Re:The Dangers of Super Smart Compilers by mikeash on Sunday December 20, 2015 @04:39PM (#10768397)
I agree that's a pretty good example the other way.

I'm pretty sure your snippet invokes nasal demons in all dialects. Dereferencing NULL is a pretty classic case where anything can happen once you do it. "Calling launch_missiles even though you don't want it to happen" is a subset of "anything," of course.

Consider what happens if you rearrange that example a bit, though. Let's say it's more like this:

    int computation(int *ptr_a, int *ptr_b) {
        if(ptr_a == NULL && ptr_b == NULL) return 0;
        if(ptr_a == NULL) return val_b;
        if(ptr_b == NULL) return val_a;
        launch_missiles(); /* OK to put this here? */
        return ...something with val_a and val_b...;
    }

    int otherComputation(int *ptr_a, int *ptr_b) {
        int val_a = *ptr_a, val_b = *ptr_b;
        int intermediate = computation(ptr_a, ptr_b);
        return ...something with val_a, val_b, and intermediate....;
    }
Then you call otherComputation in a tight loop. Should the compiler elide the NULL checks here, if computation gets inlined? otherComputation clearly requires that its parameters are non-NULL, so it should be fine to remove the checks. Failing to do so would be pretty annoying. It's clear that the inputs are constrained, and while you could fix it by factoring out the common code so that the NULL checks are explicitly optional, it would be best if the optimizer can handle it and not require us to rearrange our code to make it happy.

That's really what I'm getting at. There are clear examples where it's bad to assume undefined behavior cannot happen and optimize away checks. But there are also clear examples where it's exactly what you want to do. Can the optimizer reliably distinguish between these cases?

I think the best answer is to have tools available like the undefined behavior sanitizer which can catch these problems at runtime. You want the optimizer to make your code as fast as it legally can. Trying to tone it down is going to be error-prone, and safety checks that can't be counted on are dangerous. You might end up just moving the problem around: instead of code suddenly being optimized out because the compiler proved it can't run without invoking undefined behavior, you'll have code suddenly being optimized out because something changed that made the compiler think you intended it. By checking at runtime, you recast the problem from "does the programmer intend to pass in values which will result in undefined behavior?" to "does this code actually get called with such values?" That's way easier to check, should have zero false positives, and should have as few false negatives with good test coverage.

Re:The Dangers of Super Smart Compilers by nkurz on Sunday December 20, 2015 @05:25PM (#10768539)
Your otherComputation() example doesn't strike me as a clear cut case. My preference (and I agree that this is not required by spec) would be that the checks would be omitted only if the dereference has actually been executed, and not if the initialization has just been declared. Essentially, I think I'd like dereference of a NULL pointer to be defined behavior as far as generated code (the code must behave identically to how it would behave if the load was issued) while possibly undefined for the processor (crashing is valid, but returning a constant would be valid too). The compiler is allowed to reduce many checks to one by assuming consistency, but not allowed to ignore the checks altogether.

I'm pretty sure your snippet invokes nasal demons in all dialects. Dereferencing NULL is a pretty classic case where anything can happen once you do it.

Is the dereferencing itself undefined in all dialects, or is it only undefined if one later "evaluates" the dereferenced value? Or do some define evaluation differently? For example (to my surprise) a comment by 'lkasndasd' on the corresponding Reddit thread points out that &* ptr is never undefined behavior in C, since the address operator and the dereference operator cancel out even (explicitly) if ptr is NULL:

  6.5.3.2 Unary arithmetic operators, Semantics, paragraph 3:
  The unary & operator yields the address of its operand. 
  [...] If the operand is the result of a unary * operator, 
  neither that operator nor the & operator is evaluated and 
  the result is as if both were omitted, except that the 
  constraints on the operators still apply and the result is 
  not an lvalue.
  and footnote 102:
  Thus, &*E is equivalent to E (even if E is a null pointer) [...]
https://www.reddit.com/r/programming/comments/3xk6zj/the_dan...

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

Re:The Dangers of Super Smart Compilers by mikeash on Sunday December 20, 2015 @05:43PM (#10768609)
That quote is interesting, but the fact that they have to explicitly call it out implies to me that the mere act of writing the dereference is undefined behavior for NULL in all other cases.

I'm pretty sure the dereference is immediately UB (other than the case you quote here). Consider that when optimizations are off, that line will probably immediately execute a load instruction, generating a segfault.

Re:The Dangers of Super Smart Compilers by nkurz on Sunday December 20, 2015 @06:30PM (#10768771)
I thought it would be interesting to see how some different compilers actually behave here. This was my test program:

  nate@haswell:~/tmp$ cat deref.c
  /* gcc -fno-inline -O3 -g -Wall deref.c -o deref */
  /* (or as desired to show particular outcome) */

  #include <stdio.h>
  #include <stdlib.h>

  void deref_and_print(int *ptr) {
    int i = *ptr;  /* undefined behavior if ptr is NULL */
    if (ptr == NULL) printf("NULL\n");
    else printf("%d\n", i);
  }

  int main(int argc, char **argv) {
    int *ptr = NULL;
    if (argc <= 1) {
      printf("Usage: %s [arg]\n", argv[0]);
      printf(" Demonstrates undefined behavior if 'arg'");
      printf(" is not a positive integer\n");
      exit(1);
    }
    int i = atoi(argv[1]);
    if (i > 0) ptr = &i;
    deref_and_print(ptr);
    exit(0);
  }
As you guessed, icc, gcc, and clang all crash with a segfault for 'deref 0' when compiled with -O0. But more interestingly, when compiled with -O1, -O2, or -O3, clang prints "NULL", while icc and gcc continue to crash.
Re:The Dangers of Super Smart Compilers by mikeash on Sunday December 20, 2015 @07:34PM (#10768945)
Nice test. Surprising results for clang. I take it that it doesn't crash for you when it prints NULL? That's the case for me. I'd have thought it would either remove the NULL check and crash, or leave it in but continue to dereference NULL, and thus crash.
Re:The Dangers of Super Smart Compilers by saurik on Sunday December 20, 2015 @09:19PM (#10769227)
The code when it doesn't crash has been reorganized by the compiler so the indirection doesn't even happen until it is preparing the arguments for the printf, which is in an else after the NULL check. If you don't need the value of a computation in one of two sides of an if statement and you know there are no legitimate side effects (I say "legitimate", as crashing does not count) to the calculation, it makes sense to delay the execution of that logic until and unless you actually need it done. I thereby don't find either result surprising here: it is just a demonstration of different optimization decisions and abilities of the two compilers.