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.