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

Reply via email to