public inbox for gcc@gcc.gnu.org
 help / color / mirror / Atom feed
* cgraph: does node->inlined_to imply node->clones is non-empty?
@ 2023-03-13 10:24 Arsen Arsenović
  2023-03-14 12:42 ` Martin Liška
  2023-03-15 17:12 ` Martin Jambor
  0 siblings, 2 replies; 7+ messages in thread
From: Arsen Arsenović @ 2023-03-13 10:24 UTC (permalink / raw)
  To: gcc

[-- Attachment #1: Type: text/plain, Size: 2163 bytes --]

Hi,

I was debugging PR96059 and ran into a question which does not seem
obvious from the code.

For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
seems to try to remove an unreachable function that was already inlined
into a different unreachable function.  When the original inline
happens, ipa-inline-transform.cc:clone_inlined_nodes decides not to make
a clone, since the function being cloned is a master clone but with no
non-inline clones.

This decision later trips up the gcc_assert in:

  /* Inline clones might be kept around so their materializing allows further
     cloning.  If the function the clone is inlined into is removed, we need
     to turn it into normal cone.  */
  FOR_EACH_FUNCTION (node)
    {
      if (node->inlined_to
	  && !node->callers)
	{
	  gcc_assert (node->clones);
	  node->inlined_to = NULL;
	  update_inlined_to_pointer (node, node);
	}
      node->aux = NULL;
    }

.. because it is expecting that an inlined function without callers
(which is necessarily true here as this function is unreachable and so
was ->remove ()'d earlier) has clones.

Either removing the assertion or making clone_inline_nodes clone when
there are no existing clones 'fixes' (suppresses, but I haven't verified
whether the results are correct) the problem.

Is this gcc_assert correct in that an inlined function without callers
necessarily must have clones?

And as a side question, do all clone nodes have a ->clones pointing to
the same list of all clones, or are they in a tree-ish arrangement,
where clones of clones end up forming a tree, with the clone_of pointer
being a pointer to the parent?  (in this instance, the node that trips
the assert has a nullptr clone_of and clones value, which would AIUI
imply that it is the original)

This train of thinking doesn't end up involving any devirtualization
code, which the PR was originally reproduced with, but my current theory
is that devirtualizing here just exposed an edge case that is decently
well hidden, rather than it playing a crucial role.

Thanks in advance, have a lovely day.
-- 
Arsen Arsenović

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 381 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-13 10:24 cgraph: does node->inlined_to imply node->clones is non-empty? Arsen Arsenović
@ 2023-03-14 12:42 ` Martin Liška
  2023-03-15 17:12 ` Martin Jambor
  1 sibling, 0 replies; 7+ messages in thread
From: Martin Liška @ 2023-03-14 12:42 UTC (permalink / raw)
  To: Arsen Arsenović, gcc; +Cc: Martin Jambor, Jan Hubicka

Hi.

I'm CCing my more skilled colleagues who will know the right answer.

Cheers,
Martin

On 3/13/23 11:24, Arsen Arsenović via Gcc wrote:
> Hi,
> 
> I was debugging PR96059 and ran into a question which does not seem
> obvious from the code.
> 
> For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
> seems to try to remove an unreachable function that was already inlined
> into a different unreachable function.  When the original inline
> happens, ipa-inline-transform.cc:clone_inlined_nodes decides not to make
> a clone, since the function being cloned is a master clone but with no
> non-inline clones.
> 
> This decision later trips up the gcc_assert in:
> 
>   /* Inline clones might be kept around so their materializing allows further
>      cloning.  If the function the clone is inlined into is removed, we need
>      to turn it into normal cone.  */
>   FOR_EACH_FUNCTION (node)
>     {
>       if (node->inlined_to
> 	  && !node->callers)
> 	{
> 	  gcc_assert (node->clones);
> 	  node->inlined_to = NULL;
> 	  update_inlined_to_pointer (node, node);
> 	}
>       node->aux = NULL;
>     }
> 
> .. because it is expecting that an inlined function without callers
> (which is necessarily true here as this function is unreachable and so
> was ->remove ()'d earlier) has clones.
> 
> Either removing the assertion or making clone_inline_nodes clone when
> there are no existing clones 'fixes' (suppresses, but I haven't verified
> whether the results are correct) the problem.
> 
> Is this gcc_assert correct in that an inlined function without callers
> necessarily must have clones?
> 
> And as a side question, do all clone nodes have a ->clones pointing to
> the same list of all clones, or are they in a tree-ish arrangement,
> where clones of clones end up forming a tree, with the clone_of pointer
> being a pointer to the parent?  (in this instance, the node that trips
> the assert has a nullptr clone_of and clones value, which would AIUI
> imply that it is the original)
> 
> This train of thinking doesn't end up involving any devirtualization
> code, which the PR was originally reproduced with, but my current theory
> is that devirtualizing here just exposed an edge case that is decently
> well hidden, rather than it playing a crucial role.
> 
> Thanks in advance, have a lovely day.


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-13 10:24 cgraph: does node->inlined_to imply node->clones is non-empty? Arsen Arsenović
  2023-03-14 12:42 ` Martin Liška
@ 2023-03-15 17:12 ` Martin Jambor
  2023-03-18 15:26   ` Arsen Arsenović
  1 sibling, 1 reply; 7+ messages in thread
From: Martin Jambor @ 2023-03-15 17:12 UTC (permalink / raw)
  To: Arsen Arsenović; +Cc: GCC Mailing List, Jan Hubicka, Martin Liska

Hello,

I had been aware of your message even before Martin Liška pointed to it,
but I could not answer the questions without looking into the problem
myself.

On Mon, Mar 13 2023, Arsen Arsenović via Gcc wrote:
> Hi,
>
> I was debugging PR96059 and ran into a question which does not seem
> obvious from the code.

Thanks for looking into old bugs, it really is appreciated!

> When the original inline
> happens, ipa-inline-transform.cc:clone_inlined_nodes decides not to make
> a clone, since the function being cloned is a master clone but with no
> non-inline clones.

The reason is rather that cloning can simply be avoided if you know that
you do not need an offline copy, for anything, be it other normal calls,
calls from outside of the compilation unit, indirect calls, virtual
calls, calls through aliases, thunks... that you do not need the intact
body of the function to create other inline copies, other specialized
clones... and maybe I forgot about something.  But this is an efficiency
thing.

>
> For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
> seems to try to remove an unreachable function that was already inlined
> into a different unreachable function.

No, it fails to remove it.  It is still there but should not have been,
that is the problem.

>
> This decision later trips up the gcc_assert in:
>
>   /* Inline clones might be kept around so their materializing allows further
>      cloning.  If the function the clone is inlined into is removed, we need
>      to turn it into normal cone.  */
>   FOR_EACH_FUNCTION (node)
>     {
>       if (node->inlined_to
> 	  && !node->callers)
> 	{
> 	  gcc_assert (node->clones);
> 	  node->inlined_to = NULL;
> 	  update_inlined_to_pointer (node, node);
> 	}
>       node->aux = NULL;
>     }
>
> .. because it is expecting that an inlined function without callers
> (which is necessarily true here as this function is unreachable and so
> was ->remove ()'d earlier) has clones.

The assert makes sure that if we encounter an inlined-to node without
any caller, that it merely holds as the holder of the function body for
its other specialized (think IPA-CP) or inline clones.  If node->clones
is false, there are no such clones and it was a bug to mark the node as
required during the removal of unreachable nodes.

>
> Either removing the assertion or making clone_inline_nodes clone when
> there are no existing clones 'fixes' (suppresses, but I haven't verified
> whether the results are correct) the problem.
>
> Is this gcc_assert correct in that an inlined function without callers
> necessarily must have clones?

It is correct.  An inlined function without a caller is even a logical
oxymoron and can only exist if it has the purpose described above (and
even then probably only in a fairly special circumstances).

>
> And as a side question, do all clone nodes have a ->clones pointing to
> the same list of all clones, or are they in a tree-ish arrangement,
> where clones of clones end up forming a tree, with the clone_of pointer
> being a pointer to the parent?

The latter, they form a tree.

> (in this instance, the node that trips
> the assert has a nullptr clone_of and clones value, which would AIUI
> imply that it is the original)

Yes.

> This train of thinking doesn't end up involving any devirtualization
> code, which the PR was originally reproduced with, but my current theory
> is that devirtualizing here just exposed an edge case that is decently
> well hidden, rather than it playing a crucial role.

The inlined function is - I believe erroneously - marked as reachable by
walk_polymorphic_call_targets() within the unreachable analysis - so
devirtualizing is somewhat crucial.

I believe the real question - to which I don't have an answer yet - is
why does possible_polymorphic_call_targets return a method that is not
virtual?

  (gdb) p n->decl->decl_common.virtual_flag
  $19 = 0

Or even

  (gdb) p referenced_from_vtable_p(n)
  $24 = false

Time to dig into ipa-devirt.cc, I guess....

Martin

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-15 17:12 ` Martin Jambor
@ 2023-03-18 15:26   ` Arsen Arsenović
  2023-03-24 12:40     ` Martin Jambor
  0 siblings, 1 reply; 7+ messages in thread
From: Arsen Arsenović @ 2023-03-18 15:26 UTC (permalink / raw)
  To: Martin Jambor; +Cc: GCC Mailing List, Jan Hubicka, Martin Liska

[-- Attachment #1: Type: text/plain, Size: 6853 bytes --]

Hi Martin,

Thank you for the thorough response, and apologies for replying so late
(I'm busy most hours of most workdays nowadays).

Martin Jambor <mjambor@suse.cz> writes:

> Hello,
>
> I had been aware of your message even before Martin Liška pointed to it,
> but I could not answer the questions without looking into the problem
> myself.
>
> On Mon, Mar 13 2023, Arsen Arsenović via Gcc wrote:
>> Hi,
>>
>> I was debugging PR96059 and ran into a question which does not seem
>> obvious from the code.
>
> Thanks for looking into old bugs, it really is appreciated!

My pleasure.

>> When the original inline
>> happens, ipa-inline-transform.cc:clone_inlined_nodes decides not to make
>> a clone, since the function being cloned is a master clone but with no
>> non-inline clones.
>
> The reason is rather that cloning can simply be avoided if you know that
> you do not need an offline copy, for anything, be it other normal calls,
> calls from outside of the compilation unit, indirect calls, virtual
> calls, calls through aliases, thunks... that you do not need the intact
> body of the function to create other inline copies, other specialized
> clones... and maybe I forgot about something.  But this is an efficiency
> thing.

Right.  I was just trying to be specific about which requirement in the
if turned out to be false.

>>
>> For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
>> seems to try to remove an unreachable function that was already inlined
>> into a different unreachable function.
>
> No, it fails to remove it.  It is still there but should not have been,
> that is the problem.

Ah - I see.

>>
>> This decision later trips up the gcc_assert in:
>>
>>   /* Inline clones might be kept around so their materializing allows further
>>      cloning.  If the function the clone is inlined into is removed, we need
>>      to turn it into normal cone.  */
>>   FOR_EACH_FUNCTION (node)
>>     {
>>       if (node->inlined_to
>> 	  && !node->callers)
>> 	{
>> 	  gcc_assert (node->clones);
>> 	  node->inlined_to = NULL;
>> 	  update_inlined_to_pointer (node, node);
>> 	}
>>       node->aux = NULL;
>>     }
>>
>> .. because it is expecting that an inlined function without callers
>> (which is necessarily true here as this function is unreachable and so
>> was ->remove ()'d earlier) has clones.
>
> The assert makes sure that if we encounter an inlined-to node without
> any caller, that it merely holds as the holder of the function body for
> its other specialized (think IPA-CP) or inline clones.  If node->clones
> is false, there are no such clones and it was a bug to mark the node as
> required during the removal of unreachable nodes.

I see.  That makes sense.  So, this assert is placed here by convenience
rather than being this invariant being absolutely required for the
purpose of the loop?  (it is related, so this placement makes sense, I
just want to confirm whether it's a "mandatory" invariant)

>>
>> Either removing the assertion or making clone_inline_nodes clone when
>> there are no existing clones 'fixes' (suppresses, but I haven't verified
>> whether the results are correct) the problem.
>>
>> Is this gcc_assert correct in that an inlined function without callers
>> necessarily must have clones?
>
> It is correct.  An inlined function without a caller is even a logical
> oxymoron and can only exist if it has the purpose described above (and
> even then probably only in a fairly special circumstances).

Right.  I wasn't quite sure whether setting inlined_to would remove that
caller, but if I understood right, it should not.

What is interesting, though, is that there is an attempt to remove this
node during ipa_inline:

 (gdb) bt
 #0  cgraph_edge::remove_callee (
     this=<cgraph_edge* 0x7ffff6de0410 (<cgraph_node * 0x7ffff6dedaa0
     "__ct_base "/18> -> <cgraph_node * 0x7ffff6c5b660 "value"/28>)>)
     at ../../gcc/gcc/cgraph.h:3299
 #1 0x0000000000d03c37 in cgraph_node::remove_callees (this=<cgraph_node
  * const 0x7ffff6dedaa0 "__ct_base "/18>) at
  ../../gcc/gcc/cgraph.cc:1743
 #2 0x0000000000d04387 in cgraph_node::remove (this=<cgraph_node * const
  0x7ffff6dedaa0 "__ct_base "/18>) at ../../gcc/gcc/cgraph.cc:1884
 #3 0x00000000010da74f in symbol_table::remove_unreachable_nodes
  (this=0x7ffff6ddb000, file=0x7ffff7a814c0 <_IO_2_1_stderr_>) at
  ../../gcc/gcc/ipa.cc:518
 #4 0x0000000002b51e53 in ipa_inline () at
  ../../gcc/gcc/ipa-inline.cc:2761
 #5 0x0000000002b52cf7 in (anonymous
  namespace)::pass_ipa_inline::execute (this=0x3c8d5b0) at
  ../../gcc/gcc/ipa-inline.cc:3153
 (etc)

... I presume that my assumption that cgraph_node::remove () should
remove nodes from the cgraph_node::next chain is wrong?

>>
>> And as a side question, do all clone nodes have a ->clones pointing to
>> the same list of all clones, or are they in a tree-ish arrangement,
>> where clones of clones end up forming a tree, with the clone_of pointer
>> being a pointer to the parent?
>
> The latter, they form a tree.

I see, that makes sense.  Thanks.

>> (in this instance, the node that trips
>> the assert has a nullptr clone_of and clones value, which would AIUI
>> imply that it is the original)
>
> Yes.
>
>> This train of thinking doesn't end up involving any devirtualization
>> code, which the PR was originally reproduced with, but my current theory
>> is that devirtualizing here just exposed an edge case that is decently
>> well hidden, rather than it playing a crucial role.
>
> The inlined function is - I believe erroneously - marked as reachable by
> walk_polymorphic_call_targets() within the unreachable analysis - so
> devirtualizing is somewhat crucial.
>
> I believe the real question - to which I don't have an answer yet - is
> why does possible_polymorphic_call_targets return a method that is not
> virtual?
>
>   (gdb) p n->decl->decl_common.virtual_flag
>   $19 = 0
>
> Or even
>
>   (gdb) p referenced_from_vtable_p(n)
>   $24 = false
>
> Time to dig into ipa-devirt.cc, I guess....
>
> Martin

I saw your response on the PR itself, thanks for handling that in my
absence.  With the information you've shared I believe I understand that
the fix you provided: an inlined call can no longer be a polymorphic
call target, and so reaching an inlined function inside
walk_polymorphic_call_targets should not be possible?

I had already figured that an error could've likely been in
reach-ability analysis, but my time ran low, and I had not confirmed
anything, or as little as formalized a theory, so I just wrote the
original email instead of following this trail of thought fully.

Thank you for your guidance!  Have a lovely night :)
-- 
Arsen Arsenović

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 381 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-18 15:26   ` Arsen Arsenović
@ 2023-03-24 12:40     ` Martin Jambor
  2023-03-24 13:29       ` Jan Hubicka
  2023-03-28  1:10       ` Arsen Arsenović
  0 siblings, 2 replies; 7+ messages in thread
From: Martin Jambor @ 2023-03-24 12:40 UTC (permalink / raw)
  To: Arsen Arsenović; +Cc: GCC Mailing List, Jan Hubicka, Martin Liska

Hi,

On Sat, Mar 18 2023, Arsen Arsenović wrote:
> Martin Jambor <mjambor@suse.cz> writes:

[...]

>>>
>>> For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
>>> seems to try to remove an unreachable function that was already inlined
>>> into a different unreachable function.
>>
>> No, it fails to remove it.  It is still there but should not have been,
>> that is the problem.
>
> Ah - I see.
>
>>>
>>> This decision later trips up the gcc_assert in:
>>>
>>>   /* Inline clones might be kept around so their materializing allows further
>>>      cloning.  If the function the clone is inlined into is removed, we need
>>>      to turn it into normal cone.  */
>>>   FOR_EACH_FUNCTION (node)
>>>     {
>>>       if (node->inlined_to
>>> 	  && !node->callers)
>>> 	{
>>> 	  gcc_assert (node->clones);
>>> 	  node->inlined_to = NULL;
>>> 	  update_inlined_to_pointer (node, node);
>>> 	}
>>>       node->aux = NULL;
>>>     }
>>>
>>> .. because it is expecting that an inlined function without callers
>>> (which is necessarily true here as this function is unreachable and so
>>> was ->remove ()'d earlier) has clones.
>>
>> The assert makes sure that if we encounter an inlined-to node without
>> any caller, that it merely holds as the holder of the function body for
>> its other specialized (think IPA-CP) or inline clones.  If node->clones
>> is false, there are no such clones and it was a bug to mark the node as
>> required during the removal of unreachable nodes.
>
> I see.  That makes sense.  So, this assert is placed here by convenience
> rather than being this invariant being absolutely required for the
> purpose of the loop?  (it is related, so this placement makes sense, I
> just want to confirm whether it's a "mandatory" invariant)

If the assert fails, the algorithm does not work as intended.  OTOH, It
could be a gcc_checking_assert only since user compiled code would still
work, just would be unnecessarily bigger.

>
>>>
>>> Either removing the assertion or making clone_inline_nodes clone when
>>> there are no existing clones 'fixes' (suppresses, but I haven't verified
>>> whether the results are correct) the problem.
>>>
>>> Is this gcc_assert correct in that an inlined function without callers
>>> necessarily must have clones?
>>
>> It is correct.  An inlined function without a caller is even a logical
>> oxymoron and can only exist if it has the purpose described above (and
>> even then probably only in a fairly special circumstances).
>
> Right.  I wasn't quite sure whether setting inlined_to would remove that
> caller, but if I understood right, it should not.
>
> What is interesting, though, is that there is an attempt to remove this
> node during ipa_inline:

IPA-inline calls remove_unreachable_nodes to get rid of call graph nodes
which are known not to be necessary after inlining (inlining can lead to
redirection of some call graph edges to __builtin_unreachable) and
unreachable removal... well.. removes nodes.

>
>  (gdb) bt
>  #0  cgraph_edge::remove_callee (
>      this=<cgraph_edge* 0x7ffff6de0410 (<cgraph_node * 0x7ffff6dedaa0
>      "__ct_base "/18> -> <cgraph_node * 0x7ffff6c5b660 "value"/28>)>)
>      at ../../gcc/gcc/cgraph.h:3299
>  #1 0x0000000000d03c37 in cgraph_node::remove_callees (this=<cgraph_node
>   * const 0x7ffff6dedaa0 "__ct_base "/18>) at
>   ../../gcc/gcc/cgraph.cc:1743
>  #2 0x0000000000d04387 in cgraph_node::remove (this=<cgraph_node * const
>   0x7ffff6dedaa0 "__ct_base "/18>) at ../../gcc/gcc/cgraph.cc:1884
>  #3 0x00000000010da74f in symbol_table::remove_unreachable_nodes
>   (this=0x7ffff6ddb000, file=0x7ffff7a814c0 <_IO_2_1_stderr_>) at
>   ../../gcc/gcc/ipa.cc:518
>  #4 0x0000000002b51e53 in ipa_inline () at
>   ../../gcc/gcc/ipa-inline.cc:2761
>  #5 0x0000000002b52cf7 in (anonymous
>   namespace)::pass_ipa_inline::execute (this=0x3c8d5b0) at
>   ../../gcc/gcc/ipa-inline.cc:3153
>  (etc)
>
> ... I presume that my assumption that cgraph_node::remove () should
> remove nodes from the cgraph_node::next chain is wrong?

Ummm.... the function does that through the call to
symtab_node::unregister.  But how is that related?

>
>>>
>>> And as a side question, do all clone nodes have a ->clones pointing to
>>> the same list of all clones, or are they in a tree-ish arrangement,
>>> where clones of clones end up forming a tree, with the clone_of pointer
>>> being a pointer to the parent?
>>
>> The latter, they form a tree.
>
> I see, that makes sense.  Thanks.
>
>>> (in this instance, the node that trips
>>> the assert has a nullptr clone_of and clones value, which would AIUI
>>> imply that it is the original)
>>
>> Yes.
>>
>>> This train of thinking doesn't end up involving any devirtualization
>>> code, which the PR was originally reproduced with, but my current theory
>>> is that devirtualizing here just exposed an edge case that is decently
>>> well hidden, rather than it playing a crucial role.
>>
>> The inlined function is - I believe erroneously - marked as reachable by
>> walk_polymorphic_call_targets() within the unreachable analysis - so
>> devirtualizing is somewhat crucial.
>>
>> I believe the real question - to which I don't have an answer yet - is
>> why does possible_polymorphic_call_targets return a method that is not
>> virtual?
>>
>>   (gdb) p n->decl->decl_common.virtual_flag
>>   $19 = 0
>>
>> Or even
>>
>>   (gdb) p referenced_from_vtable_p(n)
>>   $24 = false
>>
>> Time to dig into ipa-devirt.cc, I guess....
>>
>> Martin
>
> I saw your response on the PR itself, thanks for handling that in my
> absence.  With the information you've shared I believe I understand that
> the fix you provided: an inlined call can no longer be a polymorphic
> call target, and so reaching an inlined function inside
> walk_polymorphic_call_targets should not be possible?

The thing is that ICF discovers that a virtual function, which was
originally a possible target, is semantically identically to a
non-virtual function and merges them, making the virtual function an
alias of the non-virtual one.  Devirtualization can then suddenly spit
out non-virtual potential targets which is kind of unexpected.

It seems to me that the most correct fix is to add to
walk_polymorphic_call_targets a check that the obtained possible target
is still referenced_from_vtable_p() - because the alias that was
originally a virtual function is referenced from a vtable that at this
point is also known to be gone.  But the check looks like it is possibly
expensive, so I wanted to discuss this with Honza first (hopefully next
week).

>
> I had already figured that an error could've likely been in
> reach-ability analysis, but my time ran low, and I had not confirmed
> anything, or as little as formalized a theory, so I just wrote the
> original email instead of following this trail of thought fully.
>
> Thank you for your guidance!  Have a lovely night :)

It is good thing that you asked, I also learned something new (that
virtual and non-virtual functions can be ICFed together).

Martin

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-24 12:40     ` Martin Jambor
@ 2023-03-24 13:29       ` Jan Hubicka
  2023-03-28  1:10       ` Arsen Arsenović
  1 sibling, 0 replies; 7+ messages in thread
From: Jan Hubicka @ 2023-03-24 13:29 UTC (permalink / raw)
  To: Martin Jambor; +Cc: Arsen Arsenović, GCC Mailing List, Martin Liska

Hi,
> 
> It seems to me that the most correct fix is to add to
> walk_polymorphic_call_targets a check that the obtained possible target
> is still referenced_from_vtable_p() - because the alias that was
> originally a virtual function is referenced from a vtable that at this
> point is also known to be gone.  But the check looks like it is possibly
> expensive, so I wanted to discuss this with Honza first (hopefully next
> week).

External vtables are bit special since they can refer to things that are
not accessible in current unit (static functions from other translation
units etc).
We already have and test can_refer_decl_in_current_unit_p but we test it
before alias resolution (which is there just to avoid artifically
bumping up the number of possible targets)

So perhaps following untested patch would work?

diff --git a/gcc/ipa-devirt.cc b/gcc/ipa-devirt.cc
index 14cf132c767..b33ec708d47 100644
--- a/gcc/ipa-devirt.cc
+++ b/gcc/ipa-devirt.cc
@@ -2420,7 +2420,8 @@ maybe_record_node (vec <cgraph_node *> &nodes,
       alias_target = target_node->ultimate_alias_target (&avail);
       if (target_node != alias_target
          && avail >= AVAIL_AVAILABLE
-         && target_node->get_availability ())
+         && target_node->get_availability ()
+         && can_refer_decl_in_current_unit_p (target_node->decl, NULL))
        target_node = alias_target;
     }
 
I am at conference and will be able to test it only during weekend.
Honza
> 
> >
> > I had already figured that an error could've likely been in
> > reach-ability analysis, but my time ran low, and I had not confirmed
> > anything, or as little as formalized a theory, so I just wrote the
> > original email instead of following this trail of thought fully.
> >
> > Thank you for your guidance!  Have a lovely night :)
> 
> It is good thing that you asked, I also learned something new (that
> virtual and non-virtual functions can be ICFed together).
> 
> Martin

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: cgraph: does node->inlined_to imply node->clones is non-empty?
  2023-03-24 12:40     ` Martin Jambor
  2023-03-24 13:29       ` Jan Hubicka
@ 2023-03-28  1:10       ` Arsen Arsenović
  1 sibling, 0 replies; 7+ messages in thread
From: Arsen Arsenović @ 2023-03-28  1:10 UTC (permalink / raw)
  To: Martin Jambor; +Cc: GCC Mailing List, Jan Hubicka, Martin Liska

[-- Attachment #1: Type: text/plain, Size: 8874 bytes --]

Hi,

Martin Jambor <mjambor@suse.cz> writes:

> Hi,
>
> On Sat, Mar 18 2023, Arsen Arsenović wrote:
>> Martin Jambor <mjambor@suse.cz> writes:
>
> [...]
>
>>>>
>>>> For the test case in the PR, in ipa.cc:remove_unreachable_nodes, GCC
>>>> seems to try to remove an unreachable function that was already inlined
>>>> into a different unreachable function.
>>>
>>> No, it fails to remove it.  It is still there but should not have been,
>>> that is the problem.
>>
>> Ah - I see.
>>
>>>>
>>>> This decision later trips up the gcc_assert in:
>>>>
>>>>   /* Inline clones might be kept around so their materializing allows further
>>>>      cloning.  If the function the clone is inlined into is removed, we need
>>>>      to turn it into normal cone.  */
>>>>   FOR_EACH_FUNCTION (node)
>>>>     {
>>>>       if (node->inlined_to
>>>> 	  && !node->callers)
>>>> 	{
>>>> 	  gcc_assert (node->clones);
>>>> 	  node->inlined_to = NULL;
>>>> 	  update_inlined_to_pointer (node, node);
>>>> 	}
>>>>       node->aux = NULL;
>>>>     }
>>>>
>>>> .. because it is expecting that an inlined function without callers
>>>> (which is necessarily true here as this function is unreachable and so
>>>> was ->remove ()'d earlier) has clones.
>>>
>>> The assert makes sure that if we encounter an inlined-to node without
>>> any caller, that it merely holds as the holder of the function body for
>>> its other specialized (think IPA-CP) or inline clones.  If node->clones
>>> is false, there are no such clones and it was a bug to mark the node as
>>> required during the removal of unreachable nodes.
>>
>> I see.  That makes sense.  So, this assert is placed here by convenience
>> rather than being this invariant being absolutely required for the
>> purpose of the loop?  (it is related, so this placement makes sense, I
>> just want to confirm whether it's a "mandatory" invariant)
>
> If the assert fails, the algorithm does not work as intended.  OTOH, It
> could be a gcc_checking_assert only since user compiled code would still
> work, just would be unnecessarily bigger.

Yes, that is what I was trying to ask ;-)  Apologies for failing to
articulate so.

>>
>>> It is correct.  An inlined function without a caller is even a logical
>>> oxymoron and can only exist if it has the purpose described above (and
>>> even then probably only in a fairly special circumstances).
>>
>> Right.  I wasn't quite sure whether setting inlined_to would remove that
>> caller, but if I understood right, it should not.
>>
>> What is interesting, though, is that there is an attempt to remove this
>> node during ipa_inline:
>
> IPA-inline calls remove_unreachable_nodes to get rid of call graph nodes
> which are known not to be necessary after inlining (inlining can lead to
> redirection of some call graph edges to __builtin_unreachable) and
> unreachable removal... well.. removes nodes.
>
>>
>>  (gdb) bt
>>  #0  cgraph_edge::remove_callee (
>>      this=<cgraph_edge* 0x7ffff6de0410 (<cgraph_node * 0x7ffff6dedaa0
>>      "__ct_base "/18> -> <cgraph_node * 0x7ffff6c5b660 "value"/28>)>)
>>      at ../../gcc/gcc/cgraph.h:3299
>>  #1 0x0000000000d03c37 in cgraph_node::remove_callees (this=<cgraph_node
>>   * const 0x7ffff6dedaa0 "__ct_base "/18>) at
>>   ../../gcc/gcc/cgraph.cc:1743
>>  #2 0x0000000000d04387 in cgraph_node::remove (this=<cgraph_node * const
>>   0x7ffff6dedaa0 "__ct_base "/18>) at ../../gcc/gcc/cgraph.cc:1884
>>  #3 0x00000000010da74f in symbol_table::remove_unreachable_nodes
>>   (this=0x7ffff6ddb000, file=0x7ffff7a814c0 <_IO_2_1_stderr_>) at
>>   ../../gcc/gcc/ipa.cc:518
>>  #4 0x0000000002b51e53 in ipa_inline () at
>>   ../../gcc/gcc/ipa-inline.cc:2761
>>  #5 0x0000000002b52cf7 in (anonymous
>>   namespace)::pass_ipa_inline::execute (this=0x3c8d5b0) at
>>   ../../gcc/gcc/ipa-inline.cc:3153
>>  (etc)
>>
>> ... I presume that my assumption that cgraph_node::remove () should
>> remove nodes from the cgraph_node::next chain is wrong?
>
> Ummm.... the function does that through the call to
> symtab_node::unregister.  But how is that related?

Oh - my bad.  I seem to have broken on the wrong condition here.
"value"/28 is *not* removed.

That makes more sense, it'd be confusing if remove() still lead to
FOR_EACH_FUNCTION touching a node.

>>
>>>>
>>>> And as a side question, do all clone nodes have a ->clones pointing to
>>>> the same list of all clones, or are they in a tree-ish arrangement,
>>>> where clones of clones end up forming a tree, with the clone_of pointer
>>>> being a pointer to the parent?
>>>
>>> The latter, they form a tree.
>>
>> I see, that makes sense.  Thanks.
>>
>>>> (in this instance, the node that trips
>>>> the assert has a nullptr clone_of and clones value, which would AIUI
>>>> imply that it is the original)
>>>
>>> Yes.
>>>
>>>> This train of thinking doesn't end up involving any devirtualization
>>>> code, which the PR was originally reproduced with, but my current theory
>>>> is that devirtualizing here just exposed an edge case that is decently
>>>> well hidden, rather than it playing a crucial role.
>>>
>>> The inlined function is - I believe erroneously - marked as reachable by
>>> walk_polymorphic_call_targets() within the unreachable analysis - so
>>> devirtualizing is somewhat crucial.
>>>
>>> I believe the real question - to which I don't have an answer yet - is
>>> why does possible_polymorphic_call_targets return a method that is not
>>> virtual?
>>>
>>>   (gdb) p n->decl->decl_common.virtual_flag
>>>   $19 = 0
>>>
>>> Or even
>>>
>>>   (gdb) p referenced_from_vtable_p(n)
>>>   $24 = false
>>>
>>> Time to dig into ipa-devirt.cc, I guess....
>>>
>>> Martin
>>
>> I saw your response on the PR itself, thanks for handling that in my
>> absence.  With the information you've shared I believe I understand that
>> the fix you provided: an inlined call can no longer be a polymorphic
>> call target, and so reaching an inlined function inside
>> walk_polymorphic_call_targets should not be possible?
>
> The thing is that ICF discovers that a virtual function, which was
> originally a possible target, is semantically identically to a
> non-virtual function and merges them, making the virtual function an
> alias of the non-virtual one.  Devirtualization can then suddenly spit
> out non-virtual potential targets which is kind of unexpected.
>
> It seems to me that the most correct fix is to add to
> walk_polymorphic_call_targets a check that the obtained possible target
> is still referenced_from_vtable_p() - because the alias that was
> originally a virtual function is referenced from a vtable that at this
> point is also known to be gone.  But the check looks like it is possibly
> expensive, so I wanted to discuss this with Honza first (hopefully next
> week).

I see.  That does make sense.


I'm not sure I understand Honzas suggestion, though.
can_refer_decl_in_current_unit_p is true for everything involved here,
which, AIUI, makes sense since everything is in one partition (to make
sure, I set -flto-partition=one for a test run too).

I made can_refer_decl_in_current_unit_p non-static and checked the value
for both the target_node and the alias_target in the block he mentioned,
and the result was 1 in all cases.


Thinking about this, I was curious if preventing the alias from being
followed if it would become nonvirtual would help, and it stopped the
ICE in the PR, but as with the first attempted fix of mine, I'm not
certain of its correctness:

diff --git a/gcc/ipa-devirt.cc b/gcc/ipa-devirt.cc
index 14cf132c767..36b9b87fd10 100644
--- a/gcc/ipa-devirt.cc
+++ b/gcc/ipa-devirt.cc
@@ -2420,7 +2420,9 @@ maybe_record_node (vec <cgraph_node *> &nodes,
       alias_target = target_node->ultimate_alias_target (&avail);
       if (target_node != alias_target
          && avail >= AVAIL_AVAILABLE
-         && target_node->get_availability ())
+         && target_node->get_availability ()
+         && (!DECL_VIRTUAL_P (target_node->decl)
+             || DECL_VIRTUAL_P (alias_target->decl)))
        target_node = alias_target;
     }

(also untested with the actual testsuite, it's quite late..)


Thanks again, have a lovely night.

>>
>> I had already figured that an error could've likely been in
>> reach-ability analysis, but my time ran low, and I had not confirmed
>> anything, or as little as formalized a theory, so I just wrote the
>> original email instead of following this trail of thought fully.
>>
>> Thank you for your guidance!  Have a lovely night :)
>
> It is good thing that you asked, I also learned something new (that
> virtual and non-virtual functions can be ICFed together).
>
> Martin


-- 
Arsen Arsenović

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 381 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2023-03-28  1:10 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-03-13 10:24 cgraph: does node->inlined_to imply node->clones is non-empty? Arsen Arsenović
2023-03-14 12:42 ` Martin Liška
2023-03-15 17:12 ` Martin Jambor
2023-03-18 15:26   ` Arsen Arsenović
2023-03-24 12:40     ` Martin Jambor
2023-03-24 13:29       ` Jan Hubicka
2023-03-28  1:10       ` Arsen Arsenović

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).