On Tue, Jul 11, 2023 at 10:29 AM Krister Walfridsson <krister.walfrids...@gmail.com> wrote: > > On Tue, 11 Jul 2023, Richard Biener wrote: > > >> With "plain copies", do you mean treating it as it is always defined? That > >> would prevent optimizations such as transforming > >> > >> int t; > >> if (1) > >> t = *p; > >> else > >> t = 0; > >> return t + 1; > >> > >> to the equivalent of > >> > >> return *p + 1; > >> > >> because the latter is UB if *p is undefined, but the original is OK if phi > >> nodes always give us a fully defined value. > > > > I meant the copy will simply transfer the state (uninitialized or > > initialized) > > from RHS to LHS but in itself does not invoke undefined behavior. And > > "plain" copy would be > > > > int t, u; > > t = u; > > > > where u is uninitialized and t will be as well but this copy in itself > > isn't problematic. > > So I misunderstood what you meant. This is how I have implemented it! :) > > > > Maybe we should treat these as undefined as well - the problem with PHIs is > > that the implied copies are necessary because of SSA construction even when > > they do not appear in the original program. But since transforms like > > jump threading > > can cause those copies to become degenerate it's odd to only treat those as > > OK. > > So you have to think about > > > > _1 = PHI <p_2(D)> > > > > vs. > > > > _1 = p_2(D); > > > >> > >>>> * selection: Instructions that are selecting an element (COND_EXPR, > >>>> VEC_PERM_EXPR, etc.) may select between values having uninitialized > >>>> bits, and the resulting value may have uninitialized bits. But the > >>>> condition/mask must not have uninitialized bits. > >>>> * Extraction: Instructions that are extracting bits (BIT_FIELD_REF > >>>> etc.) > >>>> may have uninitialized bits in both the input/output. > >>>> * Insertion: Instructions that are constructing/inserting values > >>>> (COMPLEX_EXPR, etc.) may have uninitialized bits in both the > >>>> input/output. > >>> > >>> Generally I think "moving" uninitialized bits in full or in part is OK. > >>> Operating on them, including comparing them (with themselves) > >>> is eventually asking for troubles. > >> > >> Makes sense. > >> > >> But I think we must restrict the definition of "move". For example, one > >> can see x * 2 as moving the uninitialized bits one step. And x + x and > >> x << 1 are the same as x * 2, so then they too would be defined? I would > >> prefer if all of them were UB if x contains uninitialized bits... > > > > I guess you have to try ... > > > >> > >>>> All other use of values having uninitialized bits are considered UB. > >>>> > >>>> Does this behavior make sense? > >>>> > >>>> The above seems to work fine so far, with one exception that can be seen > >>>> in gcc.c-torture/execute/pr108498-1.c. The test has an uninitialized bit > >>>> field > >>>> > >>>> unsigned char c6:1, c7:1, c8:1, c9:3, c10:1; > >>>> > >>>> which is written to as > >>>> > >>>> x.c6 = 0; > >>>> x.c7 = 0; > >>>> x.c8 = 0; > >>>> x.c9 = 7; > >>>> > >>>> The store merging pass changes this to > >>>> > >>>> _71 = MEM <unsigned char> [(struct C *)&x + 8B]; > >>>> _42 = _71 & 128; > >>>> _45 = _42 | 56; > >>>> > >>>> and the translation validator is now complaining that the pass has > >>>> introduced UB that was not in the original IR (because the most > >>>> significant bit in _71 is uninitialized when passed to BIT_AND_EXPR). > >>>> > >>>> I could solve this by allowing uninitialized bits in BIT_AND_EXPR and > >>>> BIT_OR_EXP, and propagating each bit according to > >>>> > >>>> * `0 & uninit` is an initialized `0` > >>>> * `1 & uninit` is uninitialized > >>>> * `0 | uninit` is uninitialized > >>>> * `1 | uninit` is an initialized `1` > >>>> > >>>> Is that the correct GIMPLE semantics? > >>> > >>> I think the above "moves" the uninitialized MSB from memory to _45 so > >>> that's OK. > >>> > >>> Some "operations" like & 0 or & 1 give either defined values or > >>> take the uninitialized bit unmodified (thus "move"). I think both > >>> kinds are OK. Likewise + 0 or * 0/1 would be OK. What's not > >>> OK is operations that use an (fully?) uninitialized value twice, > >>> like x - x when x is uninitialized. > >> > >> + 0 and * 0/1 makes sense to me. One annoyance is that it will make my > >> tool slower as it must track undefined bits in more cases. But that is > >> not a valid reason for restricting the IR semantics... > > > > * 0 produces a defined value. The difference with x * 1 is that we can > > elide the operation so the result should better behave the same with > > x itself. So I stand corrected and * 1 should not be "OK", that is > > the result is still uninitialized. With '&' a (partly) undefined value > > might become (fully) defined. > > > > A good history of us getting more "correct" here is visible in > > tree-ssa-ccp.cc:likely_value which tells CCP when a lattice > > value is UNDEFINED (CCP doesn't track undefinedness on > > a bit level so it has to treat partly undefined values as defined). > > Thanks. I'll take a look at this. > > > >>> I think we want that, as soon as the uninitialized value becomes > >>> "part" of a then partly initialized value, it's value is "fixed". > >>> With things like _Complex or vector the situation is a bit > >>> of a grey area since we have ways to operate on elements. > >>> > >>> Note that when we for example use a BIT_FIELD_REF to extract > >>> the MSB from _42 above the value would be again fully undefined. > >> > >> Do you mean that all operations on the values having "fixed" bits are > >> valid? I do not think that will work. The frozen value may be any value, > >> which mean that the program is nondeterministic. For example, if x has one > >> "fixed" uninitialized bit with the other bits 0, then code such as > >> if (x) > >> could "randomly" be either true or false, so the program will not really > >> have any defined semantic. > > > > But if (x) inspects all bits so this operation itself would invoke undefined > > behavior. > > > > Maybe a "useful" definition would be that if an operation could cause later > > program behavior to differ when you wiggle the uninitialized bits then > > that operation invokes undefined behavior. Of course that definition > > would mean a use in a PHI or in a copy invokes undefined behavior already, > > so maybe that definition plus exceptions? > > > >> And if use of an extracted "fixed" uninitialized bit is UB, then the > >> following may be UB (if x or y has a "fixed" uninitialized least > >> significant bit) > >> bool b = (x & 1) != (y & 1) > >> while the equivalent > >> bool b = (x ^ y) & 1 > >> is defined. > > > > I don't quite get the example but if there are any such transforms then > > we of course have to make sure to not introduce undefined behavior > > when it wasn't before. For example IVOPTs used to add "zero" as > > + x - x, but when 'x' is uninitialized x - x can in practice become > > non-zero. > > > > Because with PHIs we treat _1 = PHI<_2, undef> as _1 = _2 which > > menas that undef == a && undef == b && a != b can hold true. > > undefs are (not) funny. > > > >> And things may be even more fun when arithmetic is "smearing" the fixed > >> uninitialized values over the variable -- it will then be impossible to > >> determine if the extracted bits are OK or not when extracted... > > > > When taking advantage of undefinedness for optimization we have to > > error on the safe (defined) side and when restricting what ops we produce > > we have to error on the other safe (undefined) side. > > > > To give another code example, recently we've made more use of > > tree-ssa.cc:mark_ssa_maybe_undefs which marks registers that > > possibly take an undefined value in the case the point of the > > definition would not already invoked undefined behavior (at > > function boundary all incoming values are defined because (not) producing > > them at the caller side would have invoked undefined behavior). > > I think my implementation is close to what you propose -- most of the > confusion comes from our implied definitions of "frozen" and > "uninitialized bit" are slightly different. > > I'll update my implementation, and will come back with a more detailed > proposal in a few weeks when I have tried some more things.
Thanks! I've also taken the opportunity given by your work at the recent bugs to propose a talk at this years GNU Cauldron about undefined behavior on GCC and hope to at least start on documenting the state of art^WGCC in the internals manual for this. If you have any pointers to your work / research I'd be happy to point to it, learn from it (and maybe steal a word or two ;)). Thanks, Richard. > > /Krister