-3

Consider some dead-simple code (or a more complicated one, see below1) that uses an uninitialized stack variable, e.g.:

int main() { int x; return 17 / x; }

Here's what GCC emits (-O3):

mov     eax, 17
xor     ecx, ecx
cdq
idiv    ecx
ret

Here's what MSVC emits (-O2):

mov     eax, 17
cdq
idiv    DWORD PTR [rsp]
ret     0

For reference, here's what Clang emits (-O3):

ret

The thing is, all three compilers detect that this is an uninitialized variable just fine (-Wall), but only one of them actually performs any optimizations based on it.

This is kind of stumping me... I thought all the decades of fighting over undefined behavior was to allow compiler optimizations, and yet I'm seeing only one compiler cares to optimize even the most basic cases of UB.

Why is this? What do I do if I want compilers other than Clang to optimize such cases of UB? Is there any way for me to actually get the benefits of UB instead of just the downsides with either compiler?


Footnotes

1 Apparently this was too much of an SSCCE for some folks to appreciate the actual issue. If you want a more complicated example of this problem that isn't undefined on every execution of the program, just massage it a bit. e.g.:

int main(int argc, char *[])
{
    int x;
    if (argc) { x = 100 + (argc * argc + x); }
    return x;
}

On GCC you get:

main:
    xor     eax, eax
    test    edi, edi
    je      .L1
    imul    edi, edi
    lea     eax, [rdi+100]
.L1:
    ret

On Clang you get:

main:
    ret

Same issue, just more complicated.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • 7
    Since it's UB, why do you care how it optimizes it? – Ted Lyngmo Jul 03 '21 at 21:26
  • @TedLyngmo: Because I want my programs to be faster and not have to worry about undefined cases? Like I said, *"I thought all the decades of fighting over undefined behavior was to allow compiler optimizations"*... otherwise what's the point of UB? – user541686 Jul 03 '21 at 21:27
  • 7
    _"What do I do if I want compilers other than Clang to optimize such cases of UB"_ I feel like you may misunderstand what UB actually _means_. Hitting UB doesn't just tell the optimizer to "optimize this plz", hitting UB means the **whole program** becomes invalid simply for having it. At which point, what the compiler does at this point is none of your concern, since the program is in a unrecoverable and invalid state. All bets are off. – Human-Compiler Jul 03 '21 at 21:30
  • @Human-Compiler: In your opinion what's the point of UB? – user541686 Jul 03 '21 at 21:32
  • 1
    It's worth noting that optimizing out handling for a case that can't happen (such as signed integer overflow) is drastically different than optimizing out a case that's actually a logic error, such as using an uninitialized value (such as here). In your example, you're crippling the program and wanting the compiler to fix it for you -- but that's not really how C++ works – Human-Compiler Jul 03 '21 at 21:33
  • 4
    @user541686 That point of UB is to allow the compiler to assume that you haven't broken the "rules". I don't think the intention was to make it possible for programmers to actually make programs with UB and expect them to behave in any other way than undefined. – Ted Lyngmo Jul 03 '21 at 21:33
  • @TedLyngmo: What I'm trying to get at is "if the compilers are going to zero-initialize everything then why not make that the rule" instead of telling people it's UB. Isn't the point to let the compiler optimize things that it couldn't otherwise? – user541686 Jul 03 '21 at 21:35
  • 4
    @user541686 The language _doesn't_ zero-initialize values -- and `int x` does not get zero-initialized either. It's **uninitialized** which has no known value, and using it is a logical violation. The fact that different compilers treat it differently is exactly because this is undefined behavior; a rule that the compiler must assume **can't** happen. In fact, there's no guarantee that different contrived examples **on the same compiler** will produce similar output -- the compiler has no requirement to uphold that behavior. – Human-Compiler Jul 03 '21 at 21:37
  • @Human-Compiler: I already understand **what** the rules are. I am asking you to ponder **why** the rules are this way. – user541686 Jul 03 '21 at 21:38
  • 2
    The existance of UB actually does allow for optimizations, but not for programs that contain it, but for compilers: The compiler can simply assume the programmer knows what they're doing and doesn't need to yield a compilation error in the absence of defined behaviour. – fabian Jul 03 '21 at 22:01
  • Your edit still does not change anything. UB _allows_ for optimizations, it doesn't dictate it. The B is U. A compiler _can_ optimize for it, but it doesn't have to. If micro-optimizations like this matter, perhaps consider hinting it with builtins or attributes wherever possible. Or just writing code that doesn't rely on UB, which is pretty much never a good idea (especially in terms of portability). – Human-Compiler Jul 03 '21 at 22:09
  • 1
    Read __Why Is Undefined Behavior Good?__ https://blog.regehr.org/archives/213 – Richard Critten Jul 03 '21 at 22:43
  • @RichardCritten: Yup, exactly like I said: *"making it possible to generate very efficient code in certain situations"* is precisely what the compiler is failing to do here. – user541686 Jul 03 '21 at 22:44
  • 3
    @user541686 the compiler, in a detected UB situation, is under no obligation to generate any code, never mind optimised code. – Richard Critten Jul 03 '21 at 22:48
  • *Why is this? What do I do if I want compilers other than Clang to optimize such cases of UB?* -- Please read about the [as-if](https://stackoverflow.com/questions/15718262/what-exactly-is-the-as-if-rule) rule. – PaulMcKenzie Jul 03 '21 at 23:37
  • @TedLyngmo: The authors of the Standard said that the purpose was, among other things, "It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior. ". See page 11 of the rationale http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf at for more information. The notion that "non-portable or erroneous" means "non-portable, therefore erroneous" and excludes "non-portable but correct" is a gross misrepresentation of the Committee's documented intentions. – supercat Jul 04 '21 at 08:30
  • @supercat I was thinking of that which is UB. If it's been defined in a specific implementation, It's not UB there and then I agree that optimizations should follow. – Ted Lyngmo Jul 04 '21 at 08:39
  • @TedLyngmo: Modern compiler philosophy says that if the Standard characterizes something as UB, that takes priority over everything else, save only for cases that would *completely* break the language. Cases that merely create totally needless hurdles for programmers are there to forbid programmers from writing "bad" code. – supercat Jul 04 '21 at 08:55
  • Incidentally, some older versions of gcc would occasionally yield behavior inconsistent with the Standard in some cases where accessing uninitialized objects wouldn't yield UB; I suspect that adding code to unconditionally zero the objects was easier than trying to keep track of precisely when such behavior would be required. – supercat Jul 04 '21 at 09:16
  • 1
    @supercat I'm afraid you lost me there. I'm not sure what I said said that is wrong. – Ted Lyngmo Jul 04 '21 at 09:19
  • @TedLyngmo: The term "UB" is often used in these forums to refer to actions which the Standard characterizes as invoking Undefined Behavior, without regard for whether or even most implementations would have historically specified how they would behave [or might continue to do so for that matter]. – supercat Jul 04 '21 at 09:25

3 Answers3

10

Optimizing for actually reading unintiailized data is not the point.

Optimizing for assuming the data you read must have been initialized is.

So if you have some variable that can only be written to as 3 or 1, the compiler can assume it is odd.

Or, if you add positive signed constant to a signed value, we can assume the result is larger than the original signed value (this makes some loops faster).

What the optimizer does when it proves an uninitialized value is read isn't important; making UB or indeterminate value calculation faster is not the point. Well behaved programs don't do that on purpose, spending effort making it faster (or slower, or caring) is a waste of compiler writers time.

It may fall out of other efforts. Or it may not.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I feel like it's obvious that I'm not planning to ship this program verbatim, right? This kind of code block could appear in a larger program where I don't care about some particular variable being undefined because it would never happen in practice, and I wouldn't want the compiler to assume a fixed value in that case cause that'd be inefficient. I thought that was literally the entire point of UB, wasn't it? If the compiler isn't going to do that then doesn't it defeat the point of UB? All I'm doing is giving the smallest example here to repro the problem... – user541686 Jul 03 '21 at 21:40
  • 5
    The point of UB is to absolve compiler-writers from having to worry about what happens when ill-formed code is executed, so that they have as much latitude as possible to make valid code maximally efficient. Therefore there isn't anything to be gained by questioning what a compiler does when it encounters UB, because by definition it doesn't matter what the compiler does in that scenario. – Jeremy Friesner Jul 03 '21 at 21:45
  • @JeremyFriesner: The Standard uses the term "Undefined Behavior" not only to describe constructs that are erroneous (rendering portability irrelevant), but also to those that are non-portable but entirely correct on the intended target. Were it not for this, most freestanding implementations would be completely useless because the Standard defines *no* observable actions, and most implementations perform I/O by via loads and stores of addresses that don't identify storage and thus don't identify objects. Such loads and stores invoke UB, but are nonetheless correct. – supercat Jul 04 '21 at 08:36
  • Supercat iirc the term for the latter is “implementation-defined behavior”. – Jeremy Friesner Jul 04 '21 at 13:09
  • @JeremyFriesner: No, implementation-defined behaviors are those *required* to be well-defined by the implementation in a manner that fits the constraints of the standard. Like `sizeof(int)`. UB is behavior that the implementation may wish to define, but need not feel obligated to. – user541686 Jul 06 '21 at 20:13
  • @supercat It's my understanding that any program that invokes undefined behavior is considered broken, because even if it (seems to) work correctly under the current compiler/OS/hardware/etc, there is no guarantee that the next version of the compiler (or the next optimization flag somebody enables in your Makefile, or the next full moon) won't cause the program to do something completely different and undesirable. That said, if you can point me to evidence that I understand incorrectly, I'd be happy to learn about exceptions to that rule. – Jeremy Friesner Jul 06 '21 at 20:43
  • @JeremyFriesner: //Supercat iirc the term for the latter is “implementation-defined behavior”// Where does that bleeping myth come from? The authors of the Standard published a Rationale which you can read at http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf saying what they meant by "Undefined Behavior". From page 11: "[Undefined behavior] also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior." ALL non-trivial programs for freestanding implementations... – supercat Jul 06 '21 at 20:48
  • "the implementer may [...] provide a definition" --> implementer... defined – Jeremy Friesner Jul 06 '21 at 20:50
  • ...on most platforms must perform actions characterized by the Standard as UB in order to do *anything*. That's why the Standard suggests that UB may be processed in a documented fashion characteristic of the environment. On many platforms, all I/O is performed in such fashion. – supercat Jul 06 '21 at 20:50
  • 2
    @JeremyFriesner: Implementation-Defined behavior means the implementer **must** provide a definition, as distinct from UB where the implementer may do so (and in many cases should), but would not be required to do so in cases where that would be impractical. – supercat Jul 06 '21 at 20:51
  • @JeremyFriesner: Read the published Rationale document I linked, especially up through page 14 (not all that much reading, since many of the pages are nearly blank). Also read page 44 starting at line 20 for an example of a situation where the authors of the Standard expected most implementations to behave consistently in a scenario the Standard characterizes as Undefined Behavior. – supercat Jul 06 '21 at 20:56
6

Consider this example:

int foo(bool x) {
    int y;
    if (x) y = 3;
    return y;
}

Gcc realizes that the only way the function can return something well defined is when x is true. Hence, when optimizations are turned on there is no brach:

foo(bool):
        mov     eax, 3
        ret

Calling foo(true) is not undefined behavior. Calling foo(false) is undefined behavior. There is nothing in the standard that specifies why foo(false) returns 3. There is also nothing in the standard that mandates that foo(false) does not return 3. Compilers do not optimize code that has undefined behavior, but compilers can optimize code without UB (eg remove the branch in foo) because it is not specified what happens when there is UB.

What do I do if I want compilers other than Clang to optimize such cases of UB?

Compilers do that by default. Gcc is not different than Clang with respect to that.

In your example of

int main() { int x; return 17 / x; } 

there is no missed optimization, because it is not defined what the code will do in the first place.

Your second example can be considered as a missed opportunity for optimization. Though, again: UB grants opportunities to optimize code that does not have UB. The idea is not that you introduce UB in your code to gain optimizations. As your second example can (and should be) rewritten as

int main(int argc, char *[])
{
    int x = 100 + (argc * argc + x);
    return x;
}

It isnt a big issue in practice that gcc doesn't bother to remove the branch in your version. If you don't need the branch you don't have to write it just to expect the compiler to remove it.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
2

The Standard uses the term "Undefined Behavior" to refer to actions which in some contexts might be non-portable but correct, but in other contexts would be erroneous, without making any effort to distinguish into when a particular action should be viewed one way or the other.

In C89 and C99, if it would be possible for a type's underlying storage to hold an invalid bit pattern, attempting to use an uninitialized automatic-duration or malloc-allocated object of that type would invoke Undefined Behavior, but if all possible bit patterns would be valid, accessing such an object would simply yield an Unspecified Value of that type. This meant, for example, that a program could do something like:

struct ushorts256 { uint16_t dat[256]; } x,y;
void test(void)
{
  struct ushorts256 temp;
  int i;
  for (i=0; i<86; i++)
    temp.dat[i*3]=i;
  x=temp;
  y=temp;
}

and if the callers only cared about what was in multiple-of-3 elements of the structures, there would be no need to have the code worry about the other 171 values of temp.

C11 changed the rules so that compiler writers wouldn't have to follow the C89 and C99 behavior if they felt there was something more useful they could do. For example, depending upon what calling code would do with the arrays, it might be more efficient to simply have the code write every third item of x and every third item of y, leaving the remaining items alone. A consequence of this would be that non-multiple-of-3 items of x might not match the corresponding items of y, but people seeking to sell compilers were expected to be able to judge their particular customers' needs better than the Committee ever could.

Some compilers treat uninitialized objects in a manner consistent with C89 and C99. Some may exploit the freedom to have the values behave non-deterministically (as in the example above) but without not disrupting program behavior. Some may opt to treat any programs that access uninitialized variables in gratuitously meaningless fashion. Portable programs may not rely upon any particular treatment, but the authors of the Standard have expressly stated they did not wish to "demean" useful programs that happened to be non-portable (see http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf page 13).

supercat
  • 77,689
  • 9
  • 166
  • 211