Bug 93491 - Wrong optimization: const-function moved over control flow leading to crashes
Summary: Wrong optimization: const-function moved over control flow leading to crashes
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: tree-optimization (show other bugs)
Version: 10.0
: P3 normal
Target Milestone: ---
Assignee: Richard Biener
URL:
Keywords: wrong-code
Depends on:
Blocks:
 
Reported: 2020-01-29 14:29 UTC by Alexander Cherepanov
Modified: 2021-09-01 10:56 UTC (History)
1 user (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2020-01-29 00:00:00


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Alexander Cherepanov 2020-01-29 14:29:32 UTC
In the following example, the call to the function `g` is guarded by the `f(0)` call and is never evaluated but the optimizer moved it over the guard while hoisting it from the loop:

----------------------------------------------------------------------
#include <stdlib.h>

__attribute__((noipa))
void f(int i)
{
    exit(i);
}

__attribute__((const,noipa))
int g(int i)
{
    return 1 / i;
}

int main()
{
    while (1) {
        f(0);
        
        f(g(0));
    }
}
----------------------------------------------------------------------
$ gcc -std=c11 -pedantic -Wall -Wextra test.c && ./a.out
$ gcc -std=c11 -pedantic -Wall -Wextra -O3 test.c && ./a.out
Floating point exception
----------------------------------------------------------------------
gcc x86-64 version: gcc (GCC) 10.0.1 20200129 (experimental)
----------------------------------------------------------------------
Comment 1 Andrew Pinski 2020-01-29 16:26:36 UTC
Const functions by definition dont trap or throw.  So adding const to a function that traps makes the testcase undefined.

Do you have a testcase were gcc does this optimize without the user adding const and still traps?
Comment 2 Alexander Cherepanov 2020-01-29 18:11:08 UTC
(In reply to Andrew Pinski from comment #1)
> Const functions by definition dont trap or throw.

They don't trap in all cases when they are really called. But this doesn't mean they must have a sane behavior for other combinations of arguments. At least that's what I got from the definition[1] of this attribute. In particular, it reads:

"Declaring such functions with the const attribute allows GCC to avoid emitting some calls in repeated invocations of the function with the same argument values."

That is, it allow gcc to eliminate some calls, but not to introduce new ones.

But this is a gcc extension so it could be assigned any meaning that is seemed beneficial to gcc. In such a case this pr should be considered as a documentation clarification request.

[1] https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-const-function-attribute

> So adding const to a function that traps makes the testcase undefined.

Let me describe the testcase like this: the function `g` is defined only for non-zero arguments, conforms to the rules of the const attribute for non-zero arguments, and is called only with non-zero arguments, but the optimizer introduces a call to it with the zero argument.

Perhaps the minimized example that I posted looks weird. The situation could be more subtle in real life. For instance, the bad call could be due to an unitialized variable used as the argument. I mean this variable could be uninitialized only during speculative execution but perfectly initialized during ordinary execution.

Or it could be something like this:

----------------------------------------------------------------------
#include <stdlib.h>
#include <stdio.h>

__attribute__((noipa))
void check_value(int x)
{
    if (x == 0) {
        puts("Fatal error: got invalid value. Shutting down cleanly!");
        exit(0);
    }
}

__attribute__((const,noipa))
int calc(int x)
{
    return 1 / x;
}

int main(int c, char **v)
{
    (void)v;
    
    int x = 0; // invalid value by default
    // some complex initialization of x that doesn't always succeed
    if (c > 5)
        x = 123;
    // etc.

    for (int i = 0; i < 10; i++) {
        check_value(x);
        printf("result = %d\n", calc(x));
    }
}
----------------------------------------------------------------------
$ gcc -std=c11 -pedantic -Wall -Wextra test.c && ./a.out
Fatal error: got invalid value. Shutting down cleanly!
$ gcc -std=c11 -pedantic -Wall -Wextra -O3 test.c && ./a.out
Floating point exception
----------------------------------------------------------------------
gcc x86-64 version: gcc (GCC) 10.0.1 20200129 (experimental)
----------------------------------------------------------------------

Again, this doesn't mean that gcc have to support such use cases.

> Do you have a testcase were gcc does this optimize without the user adding
> const and still traps?

No. I'll file a separate bug if I stumble upon one, so please disregard this possibility for now.
Comment 3 jsm-csl@polyomino.org.uk 2020-01-29 18:14:19 UTC
On Wed, 29 Jan 2020, pinskia at gcc dot gnu.org wrote:

> Const functions by definition dont trap or throw.  So adding const to a
> function that traps makes the testcase undefined.

It's not clear to me if the definition of const means const for all 
possible arguments as opposed to const for arguments actually passed when 
the program is executed in the abstract machine (see what I said in bug 
19828 comment 12).
Comment 4 Alexander Monakov 2020-01-30 12:15:55 UTC
(In reply to Alexander Cherepanov from comment #2)
> > Do you have a testcase were gcc does this optimize without the user adding
> > const and still traps?
> 
> No. I'll file a separate bug if I stumble upon one, so please disregard this
> possibility for now.

GCC will deduce that g is const just fine, it even tells you so with -Wsuggest-attribute=const

__attribute__((noipa))
void f(int i)
{
    __builtin_exit(i);
}

__attribute__((noinline))
int g(int i)
{
    return 1 / i;
}

int main()
{
    while (1) {
        f(0);
        
        f(g(0));
    }
}

Thus removing WAITING and confirming.
Comment 5 Richard Biener 2021-09-01 09:08:15 UTC
'const' and trapping/EH are distinct bits, and definiteily tree/gimple_could_trap_p reports true for the 1 / i; stmt.  Note that traps
are not considered a "side-effect" and thus passes just checking for
gimple_has_side_effects will not get this.

But traps in general are difficult beasts, and what to do when facing them
depends on the actual transform.  In this case it is tree PRE hoisting the
g(0) call.

It's vn_reference_may_trap that handles calls optimistically:

/* Return true if the reference operation REF may trap.  */

bool
vn_reference_may_trap (vn_reference_t ref)
{
  switch (ref->operands[0].opcode)
    {
    case MODIFY_EXPR:
    case CALL_EXPR:
      /* We do not handle calls.  */
    case ADDR_EXPR:
      /* And toplevel address computations never trap.  */
      return false;

but note that tree_could_trap_p for example only looks at the actual
call instruction, not at as to whether the called function execution
might trap.  So calls need to be handled specially and we don't have
a way to test/record for whether a called function might trap.
Comment 6 Richard Biener 2021-09-01 09:32:59 UTC
Fixing this FAILs the testcase for PR88087

FAIL: gcc.dg/tree-ssa/pr88087.c scan-tree-dump pre "Eliminated: 1"

which exactly asks for this kind of transform:

int f();
int d;
void c()
{
  for (;;)
    {
      f();
      int (*fp)() __attribute__((const)) = (void *)f;
      d = fp();
    }
}

/* We shouldn't ICE and hoist the const call of fp out of the loop.  */
Comment 7 GCC Commits 2021-09-01 10:56:25 UTC
The master branch has been updated by Richard Biener <rguenth@gcc.gnu.org>:

https://gcc.gnu.org/g:13a43a90aea368a25da50762eba4873bafb4e448

commit r12-3284-g13a43a90aea368a25da50762eba4873bafb4e448
Author: Richard Biener <rguenther@suse.de>
Date:   Wed Sep 1 11:49:39 2021 +0200

    tree-optimization/93491 - avoid PRE of trapping calls across exits
    
    This makes us avoid PREing calls that could trap across other
    calls that might not return.  The PR88087 testcase has exactly
    such case so I've refactored the testcase to contain a valid PRE.
    I've also adjusted PRE to not consider pure calls possibly
    not returning in line with what we do elsewhere.
    
    Note we don't have a good idea whether a function always returns
    normally or whether its body is known to never trap.  That's
    something IPA could compute.
    
    2021-09-01  Richard Biener  <rguenther@suse.de>
    
            PR tree-optimization/93491
            * tree-ssa-pre.c (compute_avail): Set BB_MAY_NOTRETURN
            after processing the stmt itself.  Do not consider
            pure functions possibly not returning.  Properly avoid
            adding possibly trapping calls to EXP_GEN when there's
            a preceeding possibly not returning call.
            * tree-ssa-sccvn.c (vn_reference_may_trap): Conservatively
            not handle calls.
    
            * gcc.dg/torture/pr93491.c: New testcase.
            * gcc.dg/tree-ssa/pr88087.c: Change to valid PRE opportunity.
Comment 8 Richard Biener 2021-09-01 10:56:55 UTC
Fixed.