Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> The sanity checks that it removes are ones that attempt to detect UB after it has occurred.

The point is that there is still value in that - your running program can at least say "oops" after the fact. You'll at least get errors when running your tests!

Removing the `printf ("oops")` altogether under the guise of "well, something 3 lines above was UB, so we can delete the rest of the program" is what users are complaining about.

The way the compilers go about it now, is that they don't even let you fail tests because you made an oopsie a few lines ago and now everything after that is considered safe for deletion.

Look at this example:

     char *myvar = null;
     ...
     myvar[0] = 'A';
     ...
     if (myvar == null) {
        printf ("oops\n");
     }
I literally want every single one of those lines emitted in the final output.

1. I don't want the 'myvar[0]' assignment removed because the compiler determined that mvar is NULL. If it's there, at least I'll get a crash before the 'oops'. If the crash doesn't come, at least I have the 'oops' to warn in testing.

2. I don't want the `myvar == null` check removed. If the crash in the assignment didn't happen, I **want** the message 'oops' printed.

The compiler is clearly going against the intention of the programmer if it fails to emit code for any of those lines, because it turns a failing test into a passing one.



The challenge is that a compiler cannot really provide consistent "after UB occurs" behavior that you can rely on for these sorts of post-facto checks without pessimizing behavior in pretty bad ways in a lot of situations.

If you want to be able to observe values through invalid pointers then the compiler can't do things like store locals in registers since maybe you care about checking a value through one of these pointers. Huge mess! Like, whose to say that the line "myvar[0] = 'A';" doesn't write to the location in memory where the constant string "oops\n" is stored? How will the compiler ensure that your program does what you expect after a bogus write somewhere in memory?


> How will the compiler ensure that your program does what you expect after a bogus write somewhere in memory?

I'm not asking for that. I'm asking that the compiler not remove code because of a prior error!"

If the program prints garbage, that's good enough to fail on a test.

If the program cannot print anything at all, then so be it.

What's actually* happening is that an over-eager compiler is removing something that exists. I'm asking that it not do that.

I'm not asking that it ensures that the line after the error executes, I'm asking that it not be eliminated.


> I'm not asking that it ensures that the line after the error executes, I'm asking that it not be eliminated.

I'm not sure what 'eliminated' even mean here. If the pointed to object has been replaced by a register, there might not even be a pointer to check. I.e. there might not be any meaningful code the compiler could generate for the line.

Compiler transformations attempt to preserve semantics of the code, do not attempt a faithful line-by-line translation (beyond toy compilers). If there is no semantic for a line, how can the compiler translate it?

Consider this code:

  static inline int * get_x(void* ctx) { return (int*)ctx; }
  static inline int indirect( int*(*getter)(void*), void*ctx) {
    int * ptr = getter(ctx);
    if(!ptr) { return 0; } // 1
    return *ptr;
  }

  int main() { 
    int x = 10;
    return indirect(get_x, &x);
  }
After optimization, the code can be (and indeed is: https://godbolt.org/z/5vb9PdnYs) transformed to a simple "return 10". There is no meaningful translation for the line at [1] as there isn't actually any pointer or memory location the pointer could be pointing to in the translated program.

It doesn't even make sense to warn, as main, get_x and indirect could be in completely independent libraries and each make sense as written on its own (getter+ctx is just an hand built closure, so the code is far from non-realistic).


> If the pointed to object has been replaced by a register, there might not even be a pointer to check.

I dunno if that is relevant. In the example you posted, simply removing the `static inline` leaves a pointer to check. The value isn't moved to a register.

We are arguing the cases when the source lines are there, but then are not emitted. In the code samples under discussion, the pointer values are still there and can be checked. Their values are not sitting in some register.

> (getter+ctx is just an hand built closure, so the code is far from non-realistic).

Qualifying with `static inline` is unusual, and none of the objections are using static+inlined code as samples of poor code generation. The examples being presented are a lot more real (as they are taken from existing project, not contrived to make an argument).


The important bit is that the functions are inlined into main, so for that specific code path the check is removed. The fact it would be left there in the non-called function is not really relevant. I used static inline (just inline would have been enough) just to get rid of irrelevant code.

The code under discussion does not compile, so it is hard to discuss it. Please consider this variant: https://godbolt.org/z/EMzr4naMP

As you can see, while the check is still present in the non-inlined foo, main doesn't actually call it and omits the check. There is nothing left in main to check as there is no myvar object left.

This example and my previous one are similar: in both examples, not only the compiler could statically compute the value of the pointer, but it can track the pointee directly ('int x' in my example, the null pointer in your), hence it doesn't need to allocate any memory or registers for the pointer.

Separately, in both cases the compiler was able to statically prove that the pointer must be pointing to something (in my example because it knows the target, in yours because of the assignment through it), so the null check can be removed as the value of the condition is statically known to be always true.

Finally, I believe that in your example GCC found the contradiction with the pointer being both null and not null and realized that the whole main function is not possibly reachable, hence it isolates it with ud2 (clang is just silly).

What code would you expect GCC to generate in main to test the condition? Should it allocate a register, initialize it just for the purpose of performing the branch? Should it do it just for your example or also for mine?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: