Hi, 

> On Sep 1, 2025, at 05:19, Jakub Jelinek <ja...@redhat.com> wrote:
> 
> Hi!
> 
> I think I've mentioned it earlier, but -ftrivial-auto-var-init= doesn't
> work at all for C++.
You mean that -ftrivial-auto-var-init hasn’t work at all for C++’s auto 
variables with non-trivial ctors? 

> With C++26 P2795R5 being voted in, if we were to default to say
> -ftrivial-auto-var-init=zero for -std=c++26/-std=gnu++26, that would mean
> the paper isn't really implemented.

I briefly read C++26 P2795R5 this morning (still have some questions, not 
fully understand yet), my major question is:

When -ftrivial-auto-var-init=zero is combined with C++26 P2795R5, what’s the
correct behavior the user is expecting?  When reading an uninitialized variable 
is considered  an well-defined erroneous behavior, can the user still use
-ftrivial-auto-var-init to initialize the auto vairables? 
> 
> A short testcase:
> struct S { S () : a (42) {} int a, b, c; };
> struct T : S { T () : d (43) {} int d, e, f; };
> struct U : virtual S { U () : g (44) {} int g, h, i; };
> struct V : virtual U, T { V () : j (45) {} int j, k, l; };
> void bar (S &, T &, V &, int &);
> 
> int
> foo ()
> {
>  S s;
>  T t;
>  V v;
>  int i;
>  int j;
>  bar (s, t, v, i);
>  return j;
> }
> -ftrivial-auto-var-init= adds .DEFERRED_INIT ifn call for automatic
> vars which don't have DECL_INITIAL and .DEFERRED_INIT is turned into
> clearing of the var (or pattern initialization) during RTL expansion
> but for -Wuninitialized it acts as if the var is uninitialized.
> I guess it kind of can work in LLVM which doesn't have -flifetime-dse2,
> but with -flifetime-dse2 doesn't really work at all.
> 
> Take a look at the testcase after inlining ctors but before DSE1:
>  s = .DEFERRED_INIT (12, 2, &"s"[0]);
>  s ={v} {CLOBBER(bob)};
>  s.a = 42;
>  t = .DEFERRED_INIT (24, 2, &"t"[0]);
>  t ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&t] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&t].a = 42;
>  t.d = 43;
>  v = .DEFERRED_INIT (80, 2, &"v"[0]);
>  v ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 68B] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 68B].a = 42;
>  MEM[(struct U *)&v + 48B].g = 44;
>  MEM[(struct T *)&v + 8B] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 8B] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 8B].a = 42;
>  MEM[(struct T *)&v + 8B].d = 43;
>  v._vptr.V = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 32B];
>  MEM[(struct U *)&v + 48B]._vptr.U = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 
> 56B];
>  v.j = 45;
>  _1 = .DEFERRED_INIT (4, 2, &"i"[0]);
>  i = _1;
>  j_10 = .DEFERRED_INIT (4, 2, &"j"[0]);
>  bar (&s, &t, &v, &i);
> Obviously, DSE1 will remove the .DEFERRED_INIT calls for s, t and v,
> they are followed by clobbers of the vars.

When DSE1 removes the calls to .DEFERRED_INIT due to the CLOBBER after it, for 
example

s = .DEFERRED_INIT (12, 2, &"s"[0]);
s ={v} {CLOBBER(bob)};

The compiler must assume that the CLOBBER will erase the previous values of all 
bits of “s”.  
Then it’s correct to eliminate the call to .DEFERRED_INIT completely. 

However, if we want to keep the call to .DEFERRED_INIT even when “s” has a 
constructer, that
means the CLOBBER does not erase the previous values of all bits of “s”, then 
compiler SHOULD
not assume the CLOBBER (bob) will erase ALL the bits of “s”. 
So, I guess that the current assumption on the CLOBBER(bob) might need to be 
adjusted? 

> 
> Now, one possibility is drop -flifetime-dse2 for -std=c++26/-std=gnu++26,
> but besides regressing in optimizations it also means we won't be able to
> diagnose some invalid code anymore, e.g. when we don't see the
> .DEFERRED_INIT visible but are accessing something that wasn't yet
> constructed and the bob clobber would help diagnose.

I don’t think that DSE applies wrong transformation based on the current 
assumption
on CLOBBER(bob).  So, dropping lifetime-dse2 is not a good solution as my 
understanding.

I think that it’s better to adjust the assumption of CLOBBER(bob) to prevent 
the calls to
.DEFERRED_INIT from incorrectly elimination. 

Is this solution possible? 

> Another one is emit the .DEFERRED_INIT calls also (or only in for
> types with non-trivial ctors) the ctors, right after the bob CLOBBERs
> and have some optimization added which attempts to merge smaller
> .DEFERRED_INIT into a larger one with cropping the subobject clobbers
> if there are no accesses to the memory in between, so for all inlined
> ctors could turn well defined code like
>  v = .DEFERRED_INIT (80, 2, &"v"[0]);
>  v ={v} {CLOBBER(bob)};
>  v = .DEFERRED_INIT (80, 2, 0);
>  MEM[(struct S *)&v + 68B] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 68B] = .DEFERRED_INIT (12, 2, 0);
>  MEM[(struct S *)&v + 68B].a = 42;
>  MEM[(struct U *)&v + 48B].g = 44;
>  MEM[(struct T *)&v + 8B] ={v} {CLOBBER(bob)};
>  MEM[(struct T *)&v + 88B] = .DEFERRED_INIT (24, 2, 0);
>  MEM[(struct S *)&v + 8B] ={v} {CLOBBER(bob)};
>  MEM[(struct S *)&v + 88B] = .DEFERRED_INIT (12, 2, 0);
>  MEM[(struct S *)&v + 8B].a = 42;
>  MEM[(struct T *)&v + 8B].d = 43;
>  v._vptr.V = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 32B];
>  MEM[(struct U *)&v + 48B]._vptr.U = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 
> 56B];
>  v.j = 45;
> into
>  v ={v} {CLOBBER(bob)};
>  v = .DEFERRED_INIT (80, 2, &"v"[0]);
>  MEM[(struct S *)&v + 68B].a = 42;
>  MEM[(struct U *)&v + 48B].g = 44;
>  MEM[(struct S *)&v + 8B].a = 42;
>  MEM[(struct T *)&v + 8B].d = 43;
>  v._vptr.V = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 32B];
>  MEM[(struct U *)&v + 48B]._vptr.U = &MEM <int (*) ()[7]> [(void *)&_ZTV1V + 
> 56B];
>  v.j = 45;
> 
> One problem with this are [[indeterminate]] vars, if .DEFERRED_INIT is
> emitted in the ctors, the vars will be cleared even if they are
> [[indeterminate]] (unless the ctors are inlined and some optimization
> figures out, these vars are [[indeterminate]], let's drop all .DEFERRED_INIT
> calls for those and their subparts.  

If the value need to be kept as [[indeterminate]], what’s the purpose to use
-ftrivial-auto-var-init?

> On the other side, dropping
> -flifetime-dse2 would mean we don't optimize even heap allocations even when
> those are UB and not erroneous behavior.
> And another possibility would be just change the behavior of bob CLOBBERs
> if some option is enabled (either -ftrivial-auto-var-init= or whatever
> is implied for -std=c++26/-std=gnu++26), don't treat those as completely
> discarding previous content if the previous stores to the same memory
> is .DEFERRED_INIT (or some other bob CLOBBERs with .DEFERRED_INIT at the
> end).

Yes, I like this solution. 

Qing
> 
> In the C++26 paper, padding bits are still considered indeterminate, so
> the __builtin_clear_padding calls added during gimplification are
> unnecessary unless users are asking for -ftrivial-auto-var-init= is used
> explicitly.  If even padding bits were erroneous, then is_var_need_auto_init
> ignores empty types (which would be wrong for non-zero size empty types),
> and cases like x86 long double with padding bits inside of it are handled
> incorrectly by the current -ftrivial-auto-var-init= code - unless those
> vars are address taken, they are in SSA form and so their .DEFERRED_INIT
> is irrelevant, but what matters is when they are stored into memory,
> which is where HW just writes 10 bytes and not the 12 or 16.
> 
> Jakub
> 

Reply via email to