[RFC/PATCH] updating global ranges and their effect on __builtin_unreachable code

Andrew MacLeod amacleod@redhat.com
Mon Jun 7 13:37:50 GMT 2021


On 6/7/21 3:25 AM, Richard Biener wrote:
> On Wed, Jun 2, 2021 at 2:53 PM Andrew MacLeod <amacleod@redhat.com> wrote:
>> On 6/2/21 7:52 AM, Richard Biener wrote:
>>> On Wed, Jun 2, 2021 at 12:34 PM Aldy Hernandez via Gcc-patches
>>> <gcc-patches@gcc.gnu.org> wrote:
>>>> We've been having "issues" in our branch when exporting to the global
>>>> space ranges that take into account previously known ranges
>>>> (SSA_NAME_RANGE_INFO, etc).  For the longest time we had the export
>>>> feature turned off because it had the potential of removing
>>>> __builtin_unreachable code early in the pipeline.  This was causing one
>>>> or two tests to fail.
>>>>
>>>> I finally got fed up, and investigated why.
>>>>
>>>> Take the following code:
>>>>
>>>>      i_4 = somerandom ();
>>>>      if (i_4 < 0)
>>>>        goto <bb 3>; [INV]
>>>>      else
>>>>        goto <bb 4>; [INV]
>>>>
>>>>      <bb 3> :
>>>>      __builtin_unreachable ();
>>>>
>>>>      <bb 4> :
>>>>
>>>> It turns out that both legacy evrp and VRP have code that notices the
>>>> above pattern and sets the *global* range for i_4 to [0,MAX].  That is,
>>>> the range for i_4 is set, not at BB4, but at the definition site.  See
>>>> uses of assert_unreachable_fallthru_edge_p() for details.
>>>>
>>>> This global range causes subsequent passes (VRP1 in the testcase below),
>>>> to remove the checks and the __builtin_unreachable code altogether.
>>>>
>>>> // pr80776-1.c
>>>> int somerandom (void);
>>>> void
>>>> Foo (void)
>>>> {
>>>>      int i = somerandom ();
>>>>      if (! (0 <= i))
>>>>        __builtin_unreachable ();
>>>>      if (! (0 <= i && i <= 999999))
>>>>        __builtin_unreachable ();
>>>>      sprintf (number, "%d", i);
>>>> }
>>>>
>>>> This means that by the time the -Wformat-overflow warning runs, the
>>>> above sprintf has been left unguarded, and a bogus warning is issued.
>>>>
>>>> Currently the above test does not warn, but that's because of an
>>>> oversight in export_global_ranges().  This function is disregarding
>>>> known global ranges (SSA_NAME_RANGE_INFO and SSA_NAME_PTR_INFO) and only
>>>> setting ranges the ranger knows about.
>>>>
>>>> For the above test the IL is:
>>>>
>>>>      <bb 2> :
>>>>      i_4 = somerandom ();
>>>>      if (i_4 < 0)
>>>>        goto <bb 3>; [INV]
>>>>      else
>>>>        goto <bb 4>; [INV]
>>>>
>>>>      <bb 3> :
>>>>      __builtin_unreachable ();
>>>>
>>>>      <bb 4> :
>>>>      i.0_1 = (unsigned int) i_4;
>>>>      if (i.0_1 > 999999)
>>>>        goto <bb 5>; [INV]
>>>>      else
>>>>        goto <bb 6>; [INV]
>>>>
>>>>      <bb 5> :
>>>>      __builtin_unreachable ();
>>>>
>>>>      <bb 6> :
>>>>      _7 = __builtin___sprintf_chk (&number, 1, 7, "%d", i_4);
>>>>
>>>>
>>>> Legacy evrp has determined that the range for i_4 is [0,MAX] per my
>>>> analysis above, but ranger has no known range for i_4 at the definition
>>>> site.  So at export_global_ranges time, ranger leaves the [0,MAX] alone.
>>>>
>>>> OTOH, evrp sets the global range at the definition for i.0_1 to
>>>> [0,999999] per the same unreachable feature.  However, ranger has
>>>> correctly determined that the range for i.0_1 at the definition is
>>>> [0,MAX], which it then proceeds to export.  Since the current
>>>> export_global_ranges (mistakenly) does not take into account previous
>>>> global ranges, the ranges in the global tables end up like this:
>>>>
>>>> i_4: [0, MAX]
>>>> i.0_1: [0, MAX]
>>>>
>>>> This causes the first unreachable block to be removed in VRP1, but the
>>>> second one to remain.  Later VRP can determine that i_4 in the sprintf
>>>> call is [0,999999], and no warning is issued.
>>>>
>>>> But... the missing bogus warning is due to current export_global_ranges
>>>> ignoring SSA_NAME_RANGE_INFO and friends, something which I'd like to
>>>> fix.  However, fixing this, gets us back to:
>>>>
>>>> i_4: [0, MAX]
>>>> i.0_1: [0, 999999]
>>>>
>>>> Which means, we'll be back to removing the unreachable blocks and
>>>> issuing a warning in pr80776-1.c (like we have been since the beginning
>>>> of time).
>>>>
>>>> The attached patch fixes export_global_ranges to the expected behavior,
>>>> and adds the previous XFAIL to pr80776-1.c, while documenting why this
>>>> warning is issued in the first place.
>>>>
>>>> Once legacy evrp is removed, this won't be an issue, as ranges in the IL
>>>> will tell the truth.  However, this will mean that we will no longer
>>>> remove the first __builtin_unreachable combo.  But ISTM, that would be
>>>> correct behavior ??.
>>>>
>>>> BTW, in addition to this patch we could explore removing the
>>>> assert_unreachable_fallthru_edge_p() use in the evrp_analyzer, since it
>>>> is no longer needed to get the warnings in the testcases in the original
>>>> PR correctly (gcc.dg/pr80776-[12].c).
>>> But the whole point of all this singing and dancing is not to make
>>> warnings but to be able to implement assert (); or assume (); that
>>> will result in no code but optimization based on the assumption.
>>>
>>> That means that all the checks guarding __builtin_unreachable ()
>>> should be removed at the GIMPLE level - just not too early
>>> to preserve range info on the variables participating in the
>>> guarding condition.
>>>
>>> So yes, it sounds fragile but instead it's carefully architected.  Heh.
>>>
>>> In particular it is designed so that early optimization leaves those
>>> unreachable () around (for later LTO consumption and inlining, etc.
>>> to be able to re-create the ranges) whilst VRP1 / DOM will end up
>>> eliminating them.  I think we have testcases that verify said behavior,
>>> namely optimize out range checks based on the assertions - maybe missed
>>> the case where this only happens after inlining (important for your friendly
>>> C++ abstraction hell), and the unreachable()s gone.
>>>
>>> Please make sure to not break that.
>> Let me see if I understand...  we want to leave builtin_unreachable
>> around until a certain point in compilation, and then remove them?
>>
>> It seems to me that either
>>
>>    A) a builtin_unreachable () provides useful information,( ie,
>> information that isn't otherwise obvious, and would therefore stay in
>> the IL and not be eliminated by the various optimizations until it is..
>> ). OR
>>
>>    B )its telling us something is we can already figure out, in which
>> case we will naturally eliminate the branches leading to it, and then it
>> goes away on its own.
>>
>> By that standard, any builtin_unreachable that makes it late into the
>> optimization cycle is useful... So can't we just leave those until
>> whatever point it is we decide the information they provide is no longer
>> needed, and then simply drop them?  Rewrite any branch to an unreachable
>> so that its truly unreachable, and then the next cfg cleanup makes them
>> all go away?  That could be part of a late/final VRP or a pass of its own.
>>
>> This whole checking to see if they are there seems fragile, because it
>> doesn't tell us whether they are useful or not..
> It's a combination of A and B.  It's A) until after IPA and after IPA the
> information is transitioned to range information on the SSA names
> which is then persistent (but it for example does not survive all
> IPA optimizations - definitely not LTO streaming for example).  So
> we're transitioning from a if (a) __builtin_unreachable () representation
> of the useful information to representing it by on-the-side info
> (SSA_NAME_RANGE_INFO)
> on 'a'.
>
> That should work fine with ranger as well I think - now, what was fragile
> was that the way the if (a) __builtin_unreachable () IL was preserved
> was simply that there's a single EVRP pass before IPA only and the
> way it is (was) structured makes sure that once we reflect the IL on
> the range info of 'a' it is not used (in the same EVRP pass) to elide
> the condition and nothing does (did) that before VRP1 either.
>
> Richard.

OK, lets see if I follow. The problem is that if we do eliminate the if 
and __builtin_unreachable too early, LTO streaming is likely to/might 
lose the information? and maybe some of the other IPA passes will not 
maintain the ssa_name_range_info data and thus also lose the info.

So once IPA is done, we'd be free to do eliminate at will when they 
aren't useful any morer?     And if I continue to follow that logic, 
then ranger shouldn't start with global information from the 
SSA_NAME_RANGE_INFO until post-IPA... then its free to pick up that info 
and eliminate anything it can.

At least until IPA and LTO are 100% range propagators :-)  so in theory, 
we could have a flag for post-ipa that allows us to pick better 
starting  global ranges when they are available.   we could wrap that up 
with the enable_ranger() call.. checks to see what pass we're in and if 
post-ipa enables global access?  or is there such a query already available?

Andrew

PS Ah, and now that I re-read, i think we're both saying the same thing? 
:-)  so it boils down to how do we check post-ipa.

Is there any movement to making LTO maintain any range info it currently 
loses?  Maybe this is an opportunity :-)  I know it maintains some, 
because it was part of the reason we dont use globals now




More information about the Gcc-patches mailing list