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: GDV reorders null checks of 'this' with arguments #75607

Closed
jakobbotsch opened this issue Sep 14, 2022 · 7 comments · Fixed by #75634
Closed

JIT: GDV reorders null checks of 'this' with arguments #75607

jakobbotsch opened this issue Sep 14, 2022 · 7 comments · Fixed by #75634
Assignees
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Milestone

Comments

@jakobbotsch
Copy link
Member

jakobbotsch commented Sep 14, 2022

The following example throws NRE without printing any output when tiered PGO is enabled. It should print "Should be printed" before throwing NRE.

using System;
using System.Runtime.CompilerServices;
using System.Threading;

public class Program
{
    public static void Main()
    {
        for (int i = 0; i < 100; i++)
        {
            long sum = Foo(i => i * 42, true);
            if (i > 30 && i < 40)
                Thread.Sleep(100);
        }

        Foo(null, false);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long Foo(Func<long, long> f, bool silent)
    {
        long result = 0;
        for (long i = 0; i < 100000; i++)
            result += f(Test(i, silent));

        return result;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long Test(long i, bool silent)
    {
        if (i == 0 && !silent)
            Console.WriteLine("Should be printed");

        return i;
    }
}

The evaluation order is meant to be:

  1. Evaluate this
  2. Evaluate arguments
  3. Null check this
  4. Do call

With GDV, since the guard involves dereferencing 'this', step 3 happens too early. This happens for both vtable, interface and delegate GDV.

Tricky to solve unfortunately. We need to evaluate the guard after the arguments (hard to represent without lots of additional spilling) or add a null check to the guard (expensive). cc @AndyAyersMS @EgorBo

@jakobbotsch jakobbotsch self-assigned this Sep 14, 2022
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Sep 14, 2022
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Sep 14, 2022
@ghost
Copy link

ghost commented Sep 14, 2022

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

The following example throws NRE without printing any output when tiered PGO is enabled. It should print "Should be printed" before throwing NRE.

using System;
using System.Runtime.CompilerServices;
using System.Threading;

public class Program
{
    public static void Main()
    {
        for (int i = 0; i < 100; i++)
        {
            long sum = Foo(i => i * 42, true);
            if (i > 30 && i < 40)
                Thread.Sleep(100);
        }

        Foo(null, false);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long Foo(Func<long, long> f, bool silent)
    {
        long result = 0;
        for (long i = 0; i < 100000; i++)
            result += f(Test(i, silent));

        return result;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long Test(long i, bool silent)
    {
        if (i == 0 && !silent)
            Console.WriteLine("Should be printed");

        return i;
    }
}
Author: jakobbotsch
Assignees: jakobbotsch
Labels:

area-CodeGen-coreclr

Milestone: -

@jakobbotsch jakobbotsch removed the untriaged New issue has not been triaged by the area owner label Sep 14, 2022
@jakobbotsch
Copy link
Member Author

This actually makes early expansion of delegates quite hard since it requires us to evaluate all arguments with side effects before computing the new this pointer. We're not really set up to represent this well.

@jakobbotsch
Copy link
Member Author

jakobbotsch commented Sep 14, 2022

Example with interface GDV:

using System;
using System.Runtime.CompilerServices;
using System.Threading;

public class Program
{
    public static void Main()
    {
        for (int i = 0; i < 100; i++)
        {
            long sum = Foo(new C(), true);
            if (i > 30 && i < 40)
                Thread.Sleep(100);
        }

        Foo(null, false);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long Foo(IFace iface, bool silent)
    {
        long result = 0;
        for (long i = 0; i < 100000; i++)
            result += iface.Test(SideEffect(i, silent));

        return result;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static long SideEffect(long i, bool silent)
    {
        if (i == 0 && !silent)
            Console.WriteLine("Should be printed");

        return i;
    }
}

public interface IFace
{
    long Test(long arg);
}

public class C : IFace
{
    public long Test(long arg)
    {
        return arg;
    }
}

@jakobbotsch jakobbotsch changed the title JIT: Early expansion of delegate calls in GDV reorders null check of delegate with arguments JIT: GDV reorders null checks of 'this' with arguments Sep 14, 2022
@jakobbotsch
Copy link
Member Author

Early spilling might not be a problem. We already spill most arguments quite aggressively so spilling another for GDV candidates is probably not too bad.

@JulieLeeMSFT
Copy link
Member

@jakobbotsch, do we need to fix this in .NET 7?

@jakobbotsch
Copy link
Member Author

@jakobbotsch, do we need to fix this in .NET 7?

We've had this behavior for quite some time without seeing any user reports, but I'd say it's a quite significant deviation in semantics. I'll discuss it with @AndyAyersMS in a bit.

@jakobbotsch
Copy link
Member Author

We spoke offline and agreed to put this in 8.0 milestone since we haven't had any user reports in all of .NET 6 about this behavior mismatch.

@jakobbotsch jakobbotsch added this to the 8.0.0 milestone Sep 14, 2022
jakobbotsch added a commit to jakobbotsch/runtime that referenced this issue Sep 14, 2022
The normal evaluation order for a callvirt is the following:
1. The 'this' arg is evaluated
2. The arguments are evaluated
3. 'this' is null-checked
4. The call is performed

Step 1 and 2 happen as part of the IL instructions that load the
arguments, while step 3 and 4 happen as part of the callvirt IL
instruction.

For GDV the guards needs to dereference 'this'. We were doing this too
early, causing step 3 to happen before step 2. The fix is to spill all
arguments for GDVs to temps.

Fix dotnet#75607
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Sep 14, 2022
jakobbotsch added a commit that referenced this issue Oct 18, 2022
The normal evaluation order for a callvirt is the following:
1. The 'this' arg is evaluated
2. The arguments are evaluated
3. 'this' is null-checked
4. The call is performed

Step 1 and 2 happen as part of the IL instructions that load the
arguments, while step 3 and 4 happen as part of the callvirt IL
instruction.

For GDV the guards needs to dereference 'this'. We were doing this too
early, causing step 3 to happen before step 2. The fix is to spill all
side-effecting arguments for GDVs to temps.

Fix #75607
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Oct 18, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Nov 17, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants