Hi,

On Sat, Mar 18 2023, Arsen Arsenović wrote:
> Martin Jambor <mjam...@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

Reply via email to