On Sat, 28 Sep 2024, Jakub Jelinek wrote: > Hi! > > C++ has > https://eel.is/c++draft/dcl.init#general-6.2 > https://eel.is/c++draft/dcl.init#general-6.3 > which says that during zero-initialization padding bits of structures > and unions are zero initialized, and in > https://eel.is/c++draft/dcl.init#general-9.3 > says that in certain cases value-initialization is zero-initialization. > E.g. () initialization is value-initialization. > > Now, looking at C23, it has something similar, called default > initialization, which also requires zeroing padding bits, unlike C++ > in different cases. Initialization with {} initializer new in C23 is > default-initialization, and if an initializer initializes some members > but not others, the rest are also default-initialized. Reading C17, > that was apparently the case already there, eventhough it wasn't called > default initialization - "the remainder of the aggregate shall be initialized > implicitly the same as objects that have static storage duration" and > the static/thread_local storage duration initialization talking about the > clearing of padding bits. > > Of course, if this is initialization of file/namespace scope or thread_local > variable, we do zero initialize padding bits forever. A different thing are > automatic variable initializers. For structures I think whether we clear > the padding bits or not is right now purely an optimization decision during > gimplification, whether we decide to optimize by clearing the whole > structure or not from performance POV. And for unions we did clear the > padding (the whole union; the patch I've just posted changes that though). > > And I think D has something similar. > > My main question is when the standards say that the padding bits need to be > cleared, whether a valid program can actually observe that. > > I'd hope that structure assignment is element-wise copying and so doesn't > need to preserve those bits. What about memcpy, or *(unsigned char *), > or for C++ std::bit_cast inspection of the bits (constexpr for C++ or not)? > > Or is the wording about clearing padding bits in both standards just useless > and it is still UB to depend on the value of the padding bits? > > struct A { unsigned char a; unsigned int b; }; > struct B { unsigned char c; struct A d; struct A e; unsigned long long f; }; > > void > foo (void) > { > #ifdef __cplusplus > struct B a (); // value-initialization -> zero-initialization > struct B b = { 0 }; // I think this is zero-initialization of d and e and f. > #else > struct B a = {}; // New in C23, default-initialization of whole. > struct B b = { 0 }; // I think this is default-initialization of d and e > and f, > // even in C17 and possibly earlier. > #endif > /* I think both standards say that the padding in between c and d here > is cleared, can one inspect it like this? Similarly padding between e > and f. */ > if (*((unsigned char *)&a + 1) != 0) > abort (); > /* My reading of either of the standards is that the padding in between > b.c and b.d and b.e and b.f is uninitialized. */ > /* My reading of either of the standards is that the padding in between > b.d.a and b.d.b and padding between b.e.a and b.e.b is zero initialized > (and similarly also for a.d.a and a.d.b etc.). */ > if (*((unsigned char *)&b + offsetof (struct B, e) + 1) != 0) > abort (); > } > > If the padding bits need to be cleared and it needs to be observable for > C++ zero-initialization and for C default-initialization, I wonder if we > shouldn't introduce CONSTRUCTOR_CLEAR_PADDING flag, let the FEs set it on > CONSTRUCTORs where the standards guarantee clearing of padding bits and > during gimplification don't take it just as an optimization, but as a > conformance issue (say in the code in expr.cc I was touching inside of > CONSTRUCTOR_CLEAR_PADDING ctors never set *p_complete to -1, but set it to > 0 in those cases instead to force the zeroing. And a question is if we > should have some flag too for whether missing constructor elts result in > those elements to be effectively CONSTRUCTOR_CLEAR_PADDING or not (or > whether it is enough that we set *p_complete = 0 in that case anyway). > > And another question is whether we want to keep such flag on the CONSTRUCTOR > preserved in GIMPLE, or whether we declare a mere struct whatever a = {}; > in GIMPLE clears the padding too, or whether we use > MEM <char[sizeof (struct whatever)]> [&a, 0] = {}; > for that. And whether the DSE memory trimming shouldn't use some other > representation if the padding bits in it aren't needed to be cleared. > And whether we perform some optimizations which might break the padding > bits, SRA, or the DSE memory trimming (in case there is a store but some > padding bits left somewhere), etc.
On GIMPLE an aggregate assignment (which a = {} is, but also a = b) is a block copy copying all bits of the underlying storage. That's how most passes treat it (besides SRA for which we changed memcpy to aggregate copy folding to use char[] as effective type, not only forcing the alias set to zero) - especially the alias oracle treats the assignment to 'a' as a must-def of its whole storage and doesn't leave out padding as possibly not affected (thus it affects DSE). The reasoning with SRA was that even this DSE is OK since accessing padding is undefined (but what about -fno-strict-aliasing?). The classical case is void inspect_padding (Foo *); void bar (Foo *a, Foo *b) { Foo tem = *a; *b = tem; inspect_padding (b); } stores to padding of 'a' is not visible here (with -fno-strict-aliasing there could even be an object with no padding at *a), neither is what inspect_padding does. Still SRA would "totally scalarize" 'tem' and lose padding. Richard.