On Thu, 28 Aug 2025, Jason Merrill wrote:
> On 8/28/25 9:52 AM, Jakub Jelinek wrote:
> > On Thu, Aug 28, 2025 at 02:08:12PM +0200, Jason Merrill wrote:
> >>> So, do you think for P2590R2 we want a purely library implementation which
> >>> does nothing (well, define the needed templates)?
> >>> Or do you prefer some builtin which will do nothing initially but might be
> >>> changed to do something when needed?
> >>> The first version actually clears the array, not sure if that is desirable
> >>> (especially for the const, volatile and const volatile overloads).
> >>
> >> It seems undesirable: "The object representation
> >> of a is the contents of the storage prior to the call to
> >> start_lifetime_as."
> >> https://eel.is/c++draft/obj.lifetime#3
> >>
> >> So if we want to represent this somehow, it needs a different
> >> representation
> >> than the clobbers I used for placement new.
> >
> > Ah, if it actually must preserve previous bitwise content, then not sure how
> > to
> > represent it in the IL, probably needs to be some builtin which is kept in
> > the IL maybe until final.
> > For malloc etc. as well as placement new we generally don't need anything in
> > the IL for stores, they are expected to possibly change the dynamic type.
> > So say
> > long
> > foo (void *p)
> > {
> > long ret = 0;
> > long long *q = (long long *) p;
> > *q = 42;
> > ret = *q;
> > double *r = (double *) p;
> > *r = 42.0;
> > ret += *r;
> > return ret;
> > }
> > is ok, the storage has since *q = 42; long long dynamic type and the
> > *r = 42.0; store changes it to double.
> > But the way I read std::start_lifetime_as, it works as __builtin_bit_cast
> > or type puning through unions. Except for that we really require all
> > accesses to be done through the unions in that case, but with
> > struct S { int a, b; };
> > struct T { long long c; };
> > long long
> > foo (void *p)
> > {
> > S *q = std::start_lifetime_as <S> (p);
> > q->a = 1;
> > q->b = 2;
> > T *r = std::start_lifetime_as <T> (p);
> > return r->c;
> > }
> > if we implement std::start_lifetime_as just as reinterpret_cast of the
> > pointer type, then the IL will see 2 stores using S alias set (the ints
> > in there) and then read using T alias set (the long long in there).
> > One option would be to implement it using __builtin_launder though,
> > but IFN_LAUNDER is ECF_NOVOPS, so I think doesn't make it clear to the
> > middle-end that we actually do (or might) use all the stores prior to it.
> > Say
> > long long
> > foo (void *p)
> > {
> > S s;
> > s.a = 1;
> > s.b = 2;
> > T *r = std::start_lifetime_as <T> ((void *) &s);
> > return r->c;
> > }
> > if there is just
> > T *r = __builtin_launder (reinterpret_cast <T *> (void *) &s);
> > will I think happily DSE the s.a and s.b stores, while the r pointer can
> > (and does) point to the same location, the load is using a different alias
> > set.
> > Furthermore, IFN_LAUNDER is optimized away during RTL expansion, but even
> > RTL uses TBAA heavily.
>
> And we don't want launder to break TBAA, it presumes that there's already an
> object of that type at that address.
The only thing that starts lifetime of something in GIMPLE or RTL
with respect to TBAA is a store. This is actually a problem for
us when doing redundant store removal, where the only thing the
later (bit pattern preserving) store does is change the effective
type of the object.
So for std::start_lifetime_as <T> this means that either 'T' will have
to have alias-set zero or there has to be an actual store, which means
std::start_lifetime_as <T> will be more inefficient than probably
intended.
On GIMPLE the "simplest" thing is to do
MEM<T>(p) = MEM<T>((char *)p);
aka, store as 'T' but read with either the old objects effective type
or a char[] type as alias-type. But you need to hope the aggregate
copy isn't elided as useless (I think we elide memcpy (p, p, sizeof(*p))).
> > So I think we need to treat it like an IFN with pointer and size arguments
> > which to the IL acts as if it did (or could do)
> > void *
> > __builtin_start_lifetime_as (void *p, size_t s)
> > {
> > char buf[s];
> > __builtin_memcpy (buf, p, s);
> > __builtin_memcpy (p, buf, s);
> > return p;
> > }
> > or so. But we need to preserve it even during RTL.
>
> Makes sense.
That said, if we ever want to improve on this (and solve the redundant
store elimination issue), we need an actual GIMPLE/RTL statement
doing what std::start_lifetime_as <T> does (but not generate code).
Meaning, I'd vote for a p = __builtin_start_lifetime_as (p, (T *)0);
that we can eventually lower to something more sensible than a call.
This would be nothrow(). The pointer constant argument type would be
relevant (so we can use it from C as well, aka it's a template).
I do not have a good idea how to represent this cleanly on GIMPLE,
much less on RTL. The stated goal is probably to be as efficient
as the GCC supported union punning?
Richard.