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.

Reply via email to