Remix.run Logo
nneonneo 12 hours ago

Even calling uninitialized data “garbage” is misleading. You might expect that the compiler would just leave out some initialization code and compile the remaining code in the expected way, causing the values to be “whatever was in memory previously”. But no - the compiler can (and absolutely will) optimize by assuming the values are whatever would be most convenient for optimization reasons, even if it would be vanishingly unlikely or even impossible.

As an example, consider this code (godbolt: https://godbolt.org/z/TrMrYTKG9):

    struct foo {
        unsigned char a, b;
    };

    foo make(int x) {
        foo result;
        if (x) {
            result.a = 13;
        } else {
            result.b = 37;
        }
        return result;
    }
At high enough optimization levels, the function compiles to “mov eax, 9485; ret”, which sets both a=13 and b=37 without testing the condition at all - as if both branches of the test were executed. This is perfectly reasonable because the lack of initialization means the values could already have been set that way (even if unlikely), so the compiler just goes ahead and sets them that way. It’s faster!
titzer 8 hours ago | parent | next [-]

Indeed, UB is literally whatever the compiler feels like. A famous one [1] has the compiler deleting code that contains UB and falling through to the next function.

"But it's right there in the name!" Undefined behavior literally places no restrictions on the code generated or the behavior of the program. And the compiler is under no obligation to help you debug your (admittedly buggy) program. It can literally delete your program and replace it with something else that it likes.

[1] https://kristerw.blogspot.com/2017/09/why-undefined-behavior...

jmgao 11 hours ago | parent | prev | next [-]

There are some even funnier cases like this one: https://gcc.godbolt.org/z/cbscGf8ss

The compiler sees that foo can only be assigned in one place (that isn't called locally, but could called from other object files linked into the program) and its address never escapes. Since dereferencing a null pointer is UB, it can legally assume that `*foo` is always 42 and optimizes out the variable entirely.

publicdebates 11 hours ago | parent [-]

To those who are just as confused as me:

Compilers can do whatever they want when they see UB, and accessing an unassigned and unassiganble (file-local) variable is UB, therefore the compiler can just decide that *foo is in fact always 42, or never 42, or sometimes 42, and all would be just as valid options for the compiler.

(I know I'm just restating the parent comment, but I had to think it through several times before understanding it myself, even after reading that.)

jmgao 9 hours ago | parent | next [-]

> Compilers can do whatever they want when they see UB, and accessing an unassigned and unassiganble (file-local) variable is UB, therefore the compiler can just decide that *foo is in fact always 42, or never 42, or sometimes 42, and all would be just as valid options for the compiler.

That's not exactly correct. It's not that the compiler sees that there's UB and decides to do something arbitrary: it's that it sees that there's exactly one way for UB to not be triggered and so it's assuming that that's happening.

masklinn 10 hours ago | parent | prev [-]

Although it should be noted that that’s not how compilers “reason”.

The way they work things out is to assume no UB happens (because otherwise your program is invalid and you would not request compiling an invalid program would you) then work from there.

actionfromafar 6 hours ago | parent [-]

No who would write an incorrect program! :-d

sethev 6 hours ago | parent | prev | next [-]

That seems like a reasonable optimization, actually. If the programmer doesn’t initialize a variable, why not set it to a value that always works?

Good example of why uninitialized variables are not intuitive.

userbinator 7 hours ago | parent | prev | next [-]

If you don't initialise a variable, you're implicitly saying any value is fine, so this actually makes sense.

pornel 7 hours ago | parent | next [-]

The difference is that it can behave as if it had multiple different values at the same time. You don't just get any value, you can get completely absurd paradoxical Schrödinger values where `x > 5 && x < 5` may be true, and on the next line `x > 5` may be false, and it may flip on Wednesdays.

This is because the code is executed symbolically during optimization. It's not running on your real CPU. It's first "run" on a simulation of an abstract machine from the C spec, which doesn't have registers or even real stack to hold an actual garbage value, but it does have magic memory where bits can be set to 0, 1, or this-can-never-ever-happen.

Optimization passes ask questions like "is x unused? (so I can skip saving its register)" or "is x always equal to y? (so I can stop storing it separately)" or "is this condition using x always true? (so that I can remove the else branch)". When using the value is an undefined behavior, there's no requirement for these answers to be consistent or even correct, so the optimizer rolls with whatever seems cheapest/easiest.

actionfromafar 6 hours ago | parent [-]

"Your scientists were so preoccupied with whether they could, they didn't stop to think if they should."

With Optimizing settings on, the compiler should immediately treat unused variables as errors by default.

pornel 4 hours ago | parent [-]

That's what Golang went for. There are order possibilities: D has `= void` initializer to explicitly leave variables uninitialized. Rust requires values to be initialized before use, and if the compiler can't prove they are, it's either an error or requires an explicit MaybeUninit type wrapper.

like_any_other 6 hours ago | parent | prev [-]

For some values of 'sense'.

recursivecaveat 12 hours ago | parent | prev | next [-]

Even the notion that uninitialized memory contain values is kind of dangerous. Once you access them you can't reason about what's going to happen at all. Behaviour can happen that's not self-consistent with any value at all: https://godbolt.org/z/adsP4sxMT

masklinn 11 hours ago | parent [-]

Is that an old 'bot? because I noticed it was an old version of Clang, and I tried switching to the latest Clang which is hilarious: https://godbolt.org/z/fra6fWexM

nneonneo 10 hours ago | parent | next [-]

Oh yeah the classic Clang behaviour of “just stop codegen at UB”. If you look at the assembly, the main function just ends after the call to endl (right before where the if test should go); the program will run off the end of main and execute whatever nonsense is after it in memory as instructions. In this case I guess it calls main again (??) and then runs off into the woods and crashes.

I’ve never understood this behaviour from clang. At least stick a trap at the end so the program aborts instead of just executing random instructions?

The x and y values are funny too, because clang doesn’t even bother loading anything into esi for operator<<(unsigned int), so you get whatever the previous call left behind in that register. This means there’s no x or y variable at all, even though they’re nominally being “printed out”.

recursivecaveat 10 hours ago | parent | prev | next [-]

No I wrote it with the default choice of compiler just now. That newer result is truly crazy though lol.

qbane 10 hours ago | parent | prev | next [-]

icc's result is interesting too

afiori 11 hours ago | parent | prev [-]

This is gold

masklinn 11 hours ago | parent | prev | next [-]

Things can get even wonkier if the compiler keeps the values in registers, as two consecutive loads could use different registers based as you say on what's the most convenient for optimisation (register allocation, code density).

quietbritishjim 11 hours ago | parent | prev | next [-]

If I understand it right, in principle the compiler doesn't even need to do that.

It can just leave the result totally uninitialised. That's because both code paths have undefined behaviour: whichever of result.x or result.y is not set is still copied at "return result" which is undefined behaviour, so the overall function has undefined behaviour either way.

It could even just replace the function body with abort(), or omit the implementation entirely (even the ret instruction, allowing execution to just fall through to whatever memory happens to follow). Whether any computer does that in practice is another matter.

masklinn 11 hours ago | parent [-]

> It can just leave the result totally uninitialised. That's because both code paths have undefined behaviour: whichever of result.x or result.y is not set is still copied at "return result" which is undefined behaviour, so the overall function has undefined behaviour either way.

That is incorrect, per the resolution of DR222 (partially initialized structures) at WG14:

> This DR asks the question of whether or not struct assignment is well defined when the source of the assignment is a struct, some of whose members have not been given a value. There was consensus that this should be well defined because of common usage, including the standard-specified structure struct tm.

As long as the caller doesn't read an uninitialised member, it's completely fine.

11 hours ago | parent | prev | next [-]
[deleted]
arrowsmith 12 hours ago | parent | prev [-]

How is this an "optimization" if the compiled result is incorrect? Why would you design a compiler that can produce errors?

Negitivefrags 12 hours ago | parent | next [-]

It’s not incorrect.

The code says that if x is true then a=13 and if it is false than b=37.

This is the case. Its just that a=13 even if x is false. A thing that the code had nothing to say about, and so the compiler is free to do.

foltik 11 hours ago | parent [-]

Ok, so you’re saying it’s “technically correct?”

Practically speaking, I’d argue that a compiler assuming uninitialized stack or heap memory is always equal to some arbitrary convenient constant is obviously incorrect, actively harmful, and benefits no one.

publicdebates 10 hours ago | parent | next [-]

In this example, the human author clearly intended mutual exclusivity in the condition branches, and this optimization would in fact destroy that assumption. That said, (a) human intentions are not evidence of foolproof programming logic, and often miscalculate state, and (b) the author could possibly catch most or all errors here when compiling without optimizations during debugging phase.

foltik 10 hours ago | parent [-]

Regardless of intention, the code says this memory is uninitialized.

I take issue with the compiler assuming anything about the contents of that memory; it should be a black box.

masklinn 10 hours ago | parent [-]

The compiler is the arbiter of what’s what (as long as it does not run afoul the CPU itself).

The memory being uninitialised means reading it is illegal for the writer of the program. The compiler can write to it if that suits it, the program can’t see the difference without UB.

In fact the compiler can also read from it, because it knows that it has in fact initialised that memory. And the compiler is not writing a C program and is thus not bound by the strictures of the C abstract machine anyway.

foltik 9 hours ago | parent [-]

Yes yes, the spec says compilers are free to do whatever they want. That doesn’t mean they should.

> The user didn’t initialize this integer. Let’s assume it’s always 4 since that helps us optimize this division over here into a shift…

This is convenient for who exactly? Why not just treat it as a black box memory load and not do further “optimizations”?

masklinn 9 hours ago | parent [-]

> That doesn’t mean they should.

Nobody’s stopping you from using non-optimising compilers, regardless of the strawmen you assert.

foltik 9 hours ago | parent [-]

As if treating uninitialized reads as opaque somehow precludes all optimizations?

There’s a million more sensible things that the compiler could do here besides the hilariously bad codegen you see in the grandparent and sibling comments.

All I’ve heard amounts to “but it’s allowed by the spec.” I’m not arguing against that. I’m saying a spec that incentivizes this nonsense is poorly designed.

Negitivefrags 5 hours ago | parent [-]

Why is the code gen bad? What result are you wanting? You specifically want whatever value happened to be on the stack as opposed to a value the compiler picked?

1718627440 9 hours ago | parent | prev [-]

Also even without UB, even for a naive translation, a could just happen to be 13 by chance, so the behaviour isn't even an example of nasal demons.

throwatdem12311 11 hours ago | parent | prev | next [-]

Because a could be 13 even if x is false because initialisation of the struct doesn’t have defined behavior of what the initial values of a and b need to be.

Same for b. If x is true, b could be 37 no matter how unlikely that is.

xboxnolifes 9 hours ago | parent | prev | next [-]

It is not incorrect. The values are undefined, so the compiler is free to do whatever it want to do with them, even assign values to them.

tehjoker 12 hours ago | parent | prev [-]

It's not incorrect. Where is the flaw?