-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Comments
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch Issue DetailsThe 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;
}
}
|
This actually makes early expansion of delegates quite hard since it requires us to evaluate all arguments with side effects before computing the new |
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;
}
} |
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. |
@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. |
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. |
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
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
The following example throws NRE without printing any output when tiered PGO is enabled. It should print "Should be printed" before throwing NRE.
The evaluation order is meant to be:
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
The text was updated successfully, but these errors were encountered: