On Thu, 24 Sep 2020, Jason Merrill wrote: > On 9/24/20 3:43 AM, Richard Biener wrote: > > On Wed, 23 Sep 2020, Jason Merrill wrote: > > > >> On 9/23/20 2:42 PM, Richard Biener wrote: > >>> On September 23, 2020 7:53:18 PM GMT+02:00, Jason Merrill > >>> <ja...@redhat.com> > >>> wrote: > >>>> On 9/23/20 4:14 AM, Richard Biener wrote: > >>>>> C++ operator delete, when DECL_IS_REPLACEABLE_OPERATOR_DELETE_P, > >>>>> does not cause the deleted object to be escaped. It also has no > >>>>> other interesting side-effects for PTA so skip it like we do > >>>>> for BUILT_IN_FREE. > >>>> > >>>> Hmm, this is true of the default implementation, but since the function > >>>> > >>>> is replaceable, we don't know what a user definition might do with the > >>>> pointer. > >>> > >>> But can the object still be 'used' after delete? Can delete fail / throw? > >>> > >>> What guarantee does the predicate give us? > >> > >> The deallocation function is called as part of a delete expression in order > >> to > >> release the storage for an object, ending its lifetime (if it was not ended > >> by > >> a destructor), so no, the object can't be used afterward. > > > > OK, but the delete operator can access the object contents if there > > wasn't a destructor ... > > >> A deallocation function that throws has undefined behavior. > > > > OK, so it seems the 'replaceable' operators are the global ones > > (for user-defined/class-specific placement variants I see arbitrary > > extra arguments that we'd possibly need to handle). > > > > I'm happy to revert but I'd like to have a testcase that FAILs > > with the patch ;) > > > > Now, the following aborts: > > > > struct X { > > static struct X saved; > > int *p; > > X() { __builtin_memcpy (this, &saved, sizeof (X)); } > > }; > > void operator delete (void *p) > > { > > __builtin_memcpy (&X::saved, p, sizeof (X)); > > } > > int main() > > { > > int y = 1; > > X *p = new X; > > p->p = &y; > > delete p; > > X *q = new X; > > *(q->p) = 2; > > if (y != 2) > > __builtin_abort (); > > } > > > > and I could fix this by not making *p but what *p points to escape. > > The testcase is of course maximally awkward, but hey ... ;) > > > > Now this would all be moot if operator delete may not access > > the object (or if the object contents are undefined at that point). > > > > Oh, and the testcase segfaults when compiled with GCC 10 because > > there we elide the new X / delete p pair ... which is invalid then? > > Hmm, we emit > > > > MEM[(struct X *)_8] ={v} {CLOBBER}; > > operator delete (_8, 8); > > > > so the object contents are undefined _before_ calling delete > > even when I do not have a DTOR? That is, the above, > > w/o -fno-lifetime-dse, makes the PTA patch OK for the testcase. > > Yes, all classes have a destructor, even if it's trivial, so the object's > lifetime definitely ends before the call to operator delete. This is less > clear for scalar objects, but treating them similarly would be consistent with > other recent changes, so I think it's fine for us to assume that scalar > objects are also invalidated before the call to operator delete. But of > course this doesn't apply to explicit calls to operator delete outside of a > delete expression.
OK, so change the testcase main slightly to int main() { int y = 1; X *p = new X; p->p = &y; ::operator delete(p); X *q = new X; *(q->p) = 2; if (y != 2) __builtin_abort (); } in this case the lifetime of *p does not end before calling ::operator delete() and delete can stash the object contents somewhere before ending its lifetime. For the very same reason we may not elide a new/delete pair like in int main() { int *p = new int; *p = 1; ::operator delete (p); } which we before the change did not do only because calling operator delete made p escape. Unfortunately points-to analysis cannot really reconstruct whether delete was called as part of a delete expression or directly (and thus whether object lifetime ended already), neither can DCE. So I guess we need to mark the operator delete call in some way to make those transforms safe. At least currently any operator delete call makes the alias guarantee of a operator new call moot by forcing the object to be aliased with all global and escaped memory ... Looks like there are some unallocated flags for CALL_EXPR we could pick but I wonder if we can recycle protected_flag which is CALL_FROM_THUNK_P and CALL_ALLOCA_FOR_VAR_P in CALL_EXPR for calls to DECL_IS_OPERATOR_{NEW,DELETE}_P, thus whether we have CALL_FROM_THUNK_P for those operators. Guess picking a new flag is safer. But, does it seem correct that we need to distinguish delete expressions from plain calls to operator delete? In this context I also wonder about non-replaceable operator delete, specifically operator delete in classes - are there any semantic differences between those or why did we choose to only mark the replaceable ones? Thanks, Richard. > > Richard. > > > >>>>> Bootstrapped and tested on x86_64-unknown-linux-gnu, pushed. > >>>>> > >>>>> Richard. > >>>>> > >>>>> 2020-09-23 Richard Biener <rguent...@suse.de> > >>>>> > >>>>> PR tree-optimization/97151 > >>>>> * tree-ssa-structalias.c (find_func_aliases_for_call): > >>>>> DECL_IS_REPLACEABLE_OPERATOR_DELETE_P has no effect on > >>>>> arguments. > >>>>> > >>>>> * g++.dg/cpp1y/new1.C: Adjust for two more handled transforms. > >>>>> --- > >>>>> gcc/testsuite/g++.dg/cpp1y/new1.C | 4 ++-- > >>>>> gcc/tree-ssa-structalias.c | 2 ++ > >>>>> 2 files changed, 4 insertions(+), 2 deletions(-) > >>>>> > >>>>> diff --git a/gcc/testsuite/g++.dg/cpp1y/new1.C > >>>> b/gcc/testsuite/g++.dg/cpp1y/new1.C > >>>>> index aa5f647d535..fec0088cb40 100644 > >>>>> --- a/gcc/testsuite/g++.dg/cpp1y/new1.C > >>>>> +++ b/gcc/testsuite/g++.dg/cpp1y/new1.C > >>>>> @@ -69,5 +69,5 @@ test_unused() { > >>>>> delete p; > >>>>> } > >>>>> > >>>>> -/* { dg-final { scan-tree-dump-times "Deleting : operator delete" 5 > >>>> "cddce1"} } */ > >>>>> -/* { dg-final { scan-tree-dump-times "Deleting : _\\d+ = operator > >>>> new" 7 "cddce1"} } */ > >>>>> +/* { dg-final { scan-tree-dump-times "Deleting : operator delete" 6 > >>>> "cddce1"} } */ > >>>>> +/* { dg-final { scan-tree-dump-times "Deleting : _\\d+ = operator > >>>> new" 8 "cddce1"} } */ > >>>>> diff --git a/gcc/tree-ssa-structalias.c b/gcc/tree-ssa-structalias.c > >>>>> index 44fe52e0f65..f676bf91e95 100644 > >>>>> --- a/gcc/tree-ssa-structalias.c > >>>>> +++ b/gcc/tree-ssa-structalias.c > >>>>> @@ -4857,6 +4857,8 @@ find_func_aliases_for_call (struct function > >>>> *fn, gcall *t) > >>>>> point for reachable memory of their arguments. */ > >>>>> else if (flags & (ECF_PURE|ECF_LOOPING_CONST_OR_PURE)) > >>>>> handle_pure_call (t, &rhsc); > >>>>> + else if (fndecl && DECL_IS_REPLACEABLE_OPERATOR_DELETE_P > >>>> (fndecl)) > >>>>> + ; > >>>>> else > >>>>> handle_rhs_call (t, &rhsc); > >>>>> if (gimple_call_lhs (t)) > >>>>> > >>> > >> > >> > > > > > -- Richard Biener <rguent...@suse.de> SUSE Software Solutions Germany GmbH, Maxfeldstrasse 5, 90409 Nuernberg, Germany; GF: Felix Imend