Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JIT: Loop hoisting re-ordering exceptions #6639

Open
JosephTremoulet opened this issue Sep 12, 2016 · 3 comments
Open

JIT: Loop hoisting re-ordering exceptions #6639

JosephTremoulet opened this issue Sep 12, 2016 · 3 comments
Assignees
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI bug
Milestone

Comments

@JosephTremoulet
Copy link
Contributor

Loop-hoisting optimization mentions in a comment that "We assume that expressions will be hoisted so that they are evaluated in the same order as they would have been in the loop," but that assumption doesn't hold with the current implementation. Consider the following code:

using System;
using System.Runtime.CompilerServices;

namespace N
{
    public class C
    {
        int f = 2;

        [MethodImpl(MethodImplOptions.NoInlining)]
        static bool cross(int k, int y, C c)
        {
            bool b = false;

            for (int i = 0; i < k; ++i)
            {
                // Here "c.f" is invariant, but is evaluated after "i / y"
                // which may throw a different kind of exception, so can't
                // be hoisted without potentially changing the exception kind
                // thrown.
                b = (i / y < i + c.f);
            }

            return b;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        static bool swap(int k, int x, int y, C c)
        {
            bool b = false;

            for (int i = 0; i < k; ++i)
            {
                // Sub-expressions "x / y" and "c.f" are both invariant
                // w.r.t. this loop, and can be hoisted.  Since each can
                // raise an exception, and the exceptions have different
                // types, their relative order must be preserved -- the
                // hoisted copy of "x / y" must be evaluated before the
                // hoisted copy of "c.f"
                b = (x / y < i + c.f);
            }

            return b;
        }

        public static int Main(string[] args)
        {
            int errors = 0;

            try
            {
                cross(10, 0, null);
                // DivByZero should be raised from 'swap'; normal return
                // is an error.
                ++errors;
            }
            catch (DivideByZeroException)
            {
                // This is the correct result -- i / y should be evaluated and
                // raise this exception (before c.f raises nulllref).
            }
            catch
            {
                // Any exception other than DivideByZero is a failure
                ++errors;
            }

            try
            {
                swap(10, 11, 0, null);
                // DivByZero should be raised from 'swap'; normal return
                // is an error.
                ++errors;
            }
            catch (DivideByZeroException)
            {
                // This is the correct result -- x / y should be evaluated and
                // raise this exception (before c.f raises nulllref).
            }
            catch
            {
                // Any exception other than DivideByZero is a failure
                ++errors;
            }

            return 100 + errors;
        }
    }
}

In the case of method cross, there's an invariant expression in the loop which may throw, but it is preceded by a variant expression which may throw a different kind of exception; the optimizer currently performs the hoist and changes the observable exception type.

In the case of method swap, two invariant expressions are hoisted, but the order of their hoisted copies in the preheader is reversed from their order in the loop, which again is observable since the two invariant expressions can raise different types of exception.

The sequence of events leading to the swapped order in swap is:

  1. x / y < i + c.f is visited by optHoistLoopExprsForTree
    1. x / y is recursively visited. It is marked as invariant and hoistable, but not hoisted yet because we're waiting to see if the whole parent expression is invariant and hoistable.
    2. i + c.f is recursivley visited
      1. i is resursively visited, and found to be variant
      2. c.f is recursively visited, and found to be invariant and hoistable
    3. i + c.f is post-visited; it is not invariant or hoistable, but it has an invariant hoistable child c.f, which is now hoisted
  2. x / y < i + c.f is post-visited; it is not invariant or hoistable, but it has an invariant hoistable child x / y, which is now hoisted

This same sequence of events swapping the hoisted expressions in swap was responsible for swapping a static field access and a static initializer call in dotnet/corefx#11524, due to which dotnet/coreclr#6889 was reverted in dotnet/coreclr#7118. It's possible this is the same issue prompting the workaround that disable hoisting of ClsVars.

category:correctness
theme:loop-opt
skill-level:expert
cost:medium

@pgavlin
Copy link
Contributor

pgavlin commented Dec 8, 2016

@JosephTremoulet: do you think this will make 1.2?

@mikedn
Copy link
Contributor

mikedn commented Nov 3, 2018

In the case of method cross, there's an invariant expression in the loop which may throw, but it is preceded by a variant expression which may throw a different kind of exception; the optimizer currently performs the hoist and changes the observable exception type.

Here it's probably worth pointing out that hoisting can be done correctly in the following case:

b = (c.g + i / y < i + c.f);

c.g is invariant and if hoisted it will also allowing hoisting of c.f because both either throw NullRef or do not throw. The recently added extended support for exception tracking in value numbering should make this easier to implement.

In the case of method swap, two invariant expressions are hoisted, but the order of their hoisted copies in the preheader is reversed from their order in the loop, which again is observable since the two invariant expressions can raise different types of exception.

This is relatively easy to fix. When a non-hoistable node is encountered all previously encountered hoistable trees must be hoisted, not only its children.

briansull referenced this issue in briansull/coreclr Sep 30, 2019
Contributes to issue  #7147 - JIT: Loop hoisting re-ordering exceptions
Added the Test case for Issue 26417
briansull referenced this issue in briansull/coreclr Sep 30, 2019
Contributes to issue  #7147 - JIT: Loop hoisting re-ordering exceptions
Added the Test case for Issue 26417
Updated comments
briansull referenced this issue in briansull/coreclr Oct 4, 2019
Contributes to issue  #7147 - JIT: Loop hoisting re-ordering exceptions
Added the Test case for Issue 26417
Updated comments
Rebased
briansull referenced this issue in dotnet/coreclr Oct 6, 2019
…e assigned a constant value. (#26952)

* Fix issue #26417 = Incorrect caching of loop variable
Contributes to issue  #7147 - JIT: Loop hoisting re-ordering exceptions
Added the Test case for Issue 26417
SrivastavaAnubhav referenced this issue in SrivastavaAnubhav/coreclr Oct 7, 2019
…e assigned a constant value. (dotnet#26952)

* Fix issue #26417 = Incorrect caching of loop variable
Contributes to issue  #7147 - JIT: Loop hoisting re-ordering exceptions
Added the Test case for Issue 26417
@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@msftgits msftgits added this to the Future milestone Jan 31, 2020
@BruceForstall BruceForstall added the JitUntriaged CLR JIT issues needing additional triage label Oct 28, 2020
@BruceForstall BruceForstall modified the milestones: Future, 6.0.0 Jan 15, 2021
@BruceForstall BruceForstall removed the JitUntriaged CLR JIT issues needing additional triage label Jan 15, 2021
@JulieLeeMSFT JulieLeeMSFT added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Mar 23, 2021
@AndyAyersMS
Copy link
Member

@BruceForstall still planning on fixing this in 6.0?

@BruceForstall BruceForstall modified the milestones: 6.0.0, Future May 27, 2021
@JulieLeeMSFT JulieLeeMSFT removed the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Jun 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI bug
Projects
None yet
Development

No branches or pull requests

7 participants