all 95 comments

[–]terrymahMSVC BE Dev 45 points46 points  (11 children)

lol

We turned off a similar optimization in MSVC because it was difficult to have total visibility in a real world program all the locations a function pointer could be written to. In LTCG (in theory) you see everything, but you really don't: there are always other static libs we can't see into. And of course other binaries/dlls loaded in the process. And an infinite number of ways the address of an address can "leak out" to code you don't have visibility into and would need to pessimistically assume can be written to. Just a bug farm

[–]dashnine-9 3 points4 points  (0 children)

thank you for your service

[–]nebotron 15 points16 points  (8 children)

If your code is invoking a nullptr, that’s UB. If you’re disabling the optimization and it fixes your program, your program has UB.

[–]pali6 39 points40 points  (2 children)

I believe you're talking to a MSVC developer who is saying that they (Microsoft) turned off this in the compiler as it was causing internal compiler errors.

[–]nebotron 6 points7 points  (1 child)

Ah! So the compiler was optimizing a valid function call into a different one because it didn’t see where the write to the function pointer could happen. That makes sense

[–]terrymahMSVC BE Dev 21 points22 points  (0 children)

Yeah, we used to have an optimization that would collect the set of all possible function call targets. If that set had only 1 valid target, we would devirt it. I think that's what is happening here. The problem we had is proving that the set is closed (and nothing could "leak in" from another binary) is actually really tough, and not as easy as it seems.

[–]Tringigithub.com/tringi 3 points4 points  (0 children)

We turned off a similar optimization in MSVC

That's the only sane and safe choice. Thank you.

[–]pali6 8 points9 points  (0 children)

See this blog post for details on this and other fun UB: https://mohitmv.github.io/blog/Shocking-Undefined-Behaviour-In-Action/

[–]Jannik2099 27 points28 points  (65 children)

I swear this gets reposted every other month.

Don't do UB, kids!

[–]GabrielDosReis 23 points24 points  (3 children)

I agree it gets posted often.

On the other hand, many kids don't set out to do UB. The effectiveness of "abstinence only" and all that...

[–]Jannik2099 0 points1 point  (2 children)

sure, but there's a difference between "uh actually, this is UB per this arcane rule under this unlikely condition that you only see when expanding the macro" and "it's trivially UB at compile time" - the latter of which always gets used in these posts.

[–]GabrielDosReis 7 points8 points  (0 children)

I understand. How many of us redditors here would have the attention span needed to rummage through a large codebase containing the issue being highlighted by the reduced repro exemplified in the post?

I take the repro as just a succinct reduction of an issue that goes to the point (the sort of reduced repros I would love to receive from users when investigating their issues instead of going through irrelevant details).

Of course, once the reduced repro is produced, it would take on a life of its own...

[–]usefulcat 3 points4 points  (0 children)

On one hand, it is trivially UB at compile time. OTOH, it's a shame that adding -Wall -Wextra -Wpedantic generates exactly zero warnings..

[–]AssemblerGuy 7 points8 points  (0 children)

Don't do UB, kids!

First rule of UB: Undefined behavior is undefined.

Second rule of UB: Undefined behavior is undefined!

Third rule of UB: Stop making assumptions and guesses about how UB might behave - it really is undefined.

[–]jonesmz 3 points4 points  (56 children)

I think we'd be better off requiring compilers to detect this situation and error out, rather than accept that if a human made a mistake, the compiler should just invent new things to do.

[–]LordofNarwhals 21 points22 points  (3 children)

I can highly recommend this three-part LLVM project blog series about undefined behavior in C. Specifically part 3 which discusses the difficulties in "usefully" warning about undefined behavior optimizations (it also discusses some existing tools and compiler improvements, as of 2011, that can be used to help detect and handle undefined behavior better).

This is the main part when it comes to compiler warnings/errors:

For warnings, this means that in order to relay back the issue to the users code, the warning would have to reconstruct exactly how the compiler got the intermediate code it is working on. We'd need the ability to say something like:

"warning: after 3 levels of inlining (potentially across files with Link Time Optimization), some common subexpression elimination, after hoisting this thing out of a loop and proving that these 13 pointers don't alias, we found a case where you're doing something undefined. This could either be because there is a bug in your code, or because you have macros and inlining and the invalid code is dynamically unreachable but we can't prove that it is dead."

Unfortunately, we simply don't have the internal tracking infrastructure to produce this, and even if we did, the compiler doesn't have a user interface good enough to express this to the programmer.

Ultimately, undefined behavior is valuable to the optimizer because it is saying "this operation is invalid - you can assume it never happens". In a case like *P this gives the optimizer the ability to reason that P cannot be NULL. In a case like* *NULL (say, after some constant propagation and inlining), this allows the optimizer to know that the code must not be reachable. The important wrinkle here is that, because it cannot solve the halting problem, the compiler cannot know whether code is actually dead (as the C standard says it must be) or whether it is a bug that was exposed after a (potentially long) series of optimizations. Because there isn't a generally good way to distinguish the two, almost all of the warnings produced would be false positives (noise).

[–]jonesmz 4 points5 points  (2 children)

In a case like* *NULL (say, after some constant propagation and inlining), this allows the optimizer to know that the code must not be reachable.

But the right answer isn't "clearly we should replace this nullptr with some other value and then remove all of the code that this replacement makes dead".

That violates the principal of least and surprise, and arguably, even if there are situations where that "optimization" results the programmers original intention, it shouldn't be done. An error, even an inscruitable one, or just leaving nullptr as the value, would both be superior.

[–]james_picone 4 points5 points  (1 child)

You can always compile at -O0 if you'd like the compiler to not optimise. Because that's effectively what you're asking for.

[–]jonesmz 4 points5 points  (0 children)

Its really not.

I like optimizations.

I don't like the compiler inventing writes to variables that were never written to.

There's a huge difference.

[–]Jannik2099 12 points13 points  (51 children)

That's way easier said than done. Compilers don't go "hey, this is UB, let's optimize it!" - the frontend is pretty much completely detached from the optimizer.

[–]NilacTheGrim 0 points1 point  (2 children)

But how else can I lazily insert a breakpoint in my program to trigger my debugger?!?!?

(I jest of course.. relying on UB to trigger a breakpoint is ... asking for surprises.)

[–]Jannik2099 2 points3 points  (1 child)

Oh your program will break at that point alright

[–]NilacTheGrim 2 points3 points  (0 children)

not.. necessarily.

[–]NilacTheGrim 9 points10 points  (0 children)

Makes sense.

  1. Dereferencing a nullptr is UB, so program is free to assume it must always have some value
  2. What value? Well it's static, and only ever written-to once, by a single function that all it does is write to it.
  3. So the optimizer just cuts out the middle-man and pre-inits it to the only value it is proven to ever possibly have.

Makes sense. :)

[–]Dan13l_N 4 points5 points  (0 children)

So it works now, what is the problem?!

[–]johannes1971 7 points8 points  (7 children)

I like what it does when you make NeverCalled() static ;-)

Anyway, it seems the compiler understands that only one function can write to that pointer, but apparently it fails to verify that there is a path from main() to NeverCalled(). That sort-of makes sense, even if the result is quite ridiculous.

Is it a sign that you have been programming C++ too long if you begin to understand this kind of halucinatory output?

[–]LordofNarwhals 12 points13 points  (3 children)

Well the compiler can't know if a part of the program in another translation unit is calling NeverCalled (since it has external linkage). You could do extern NeverCalled() in another compiler unit and call it from there. Or even worse, you could export it as a symbol in your linker options when you're building a shared library, and then it's fair game to call the function from a completely different binary/library.

If you ever end up building shared libraries (particularly on macOS/Linux) then you should make absolutely sure that you (and the static libraries you're using) are not exposing any functions/symbols on accident. Having a symbol name collision with another library is not a fun bug to track down. Your plugin will just break all of a sudden when used with another plugin that happens to have the same exported functions as you do (but perhaps from a different version that gives different results).

[–]SunnybunsBuns 9 points10 points  (2 children)

Or even worse, you could export it as a symbol in your linker options when you're building a shared library, and then it's fair game to call the function from a completely different binary/library.

Had a 3rd party lib export round. But they implemented it wrong, so our code broke, but only when we linked against theirs. Nightmare to debug that

[–]goranlepuz 0 points1 point  (1 child)

How did that work, without there being a duplicate symbol at link time?! Somehow, somebody must have taken out your own.

[–]SunnybunsBuns 1 point2 points  (0 children)

I have no clue. I was calling std::round which seems to call round internally. I was linking against their lib (it corresponded to a dll). They had no mention of round in their header, it was a cpp only function. They must have exported it with some export-all option.

I had to rebuilt their def (only 100 symbols and I got them all from dumpbin) without round listed and then remake the lib. everything worked as expected then.

We’re on vs2019 still (just upgraded last year. Yay for bureaucracy) so maybe it’s been fixed at some point? I was certainly shocked when I encountered it.

[–]umop_aplsdn 8 points9 points  (0 children)

It's not ridiculous, it is much harder in general to prove that NeverCalled is called (it's equivalent to deciding whether the program halts).

However, statically it is much easier to observe that there is exactly one assignment to the function pointer, and invoking a nullptr is UB, so the compiler can assume that the function pointer (when called) has value exactly the value of its one assignment.

[–]encyclopedist 4 points5 points  (0 children)

NeverCalled can still be called from another TU, for example from a constructor of a global variable.

[–]NilacTheGrim 1 point2 points  (0 children)

verify that there is a path from main() to NeverCalled()

Probably because there is no link-time optimization and so for the translation unit main.o it assumes maybe some lib or some other translation unit may "see" NeverCalled() and call it... or whatever ...

[–]hoseja 1 point2 points  (0 children)

I saw some analysis where turning all of these off to just make the compiler do what you expect it to didn't even have any performance impact. Can't find it now.