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: teach VN to fold type comparisons #72136

Merged
merged 13 commits into from
Jul 25, 2022

Conversation

AndyAyersMS
Copy link
Member

Teach VN to fold some comparisons involving calls to TypeHandleToRuntimeType.

Closes #71909.

@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 Jul 13, 2022
@ghost ghost assigned AndyAyersMS Jul 13, 2022
@ghost
Copy link

ghost commented Jul 13, 2022

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

Issue Details

Teach VN to fold some comparisons involving calls to TypeHandleToRuntimeType.

Closes #71909.

Author: AndyAyersMS
Assignees: -
Labels:

area-CodeGen-coreclr

Milestone: -

@AndyAyersMS
Copy link
Member Author

cc @dotnet/jit-contrib

Could defer this until RC snap. Not sure how strict we're going to be.

// Value number counterpart to gtFoldTypeCompare
// Doesn't handle all the cases (yet).
//
// (EQ/NE (TypeHandleToRuntimeType x) (TypeHandleToRuntimeType y)) == (EQ/NE x y)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this function is injective, e.g. all function pointers map to typeof(IntPtr).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we should check with the runtime via compareTypesForEquality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this function is injective, e.g. all function pointers map to typeof(IntPtr).

Worth to note that all function pointer typeofs currently compare as equal.

@AndyAyersMS
Copy link
Member Author

Seems like the jit gets this case wrong even without this new optimization:

typeof(delegate*<int, double>) == typeof(delegate* unmanaged<float, void*, void>);

we fold this in the importer when optimizing:

Folding call to Type:op_Equality to a simple compare via EQ
Optimizing compare of types-from-handles to instead compare handles
Asking runtime to compare 00007FFEBD6C594A (System.UIntPtr) and 00007FFEBD6C59C2 (System.UIntPtr) for equality
Runtime reports comparison is known at jit time: 0

Seems like the jit interface methods canInlineTypeCheck and compareTypesForEquality both might need updating, since type handles may be distinct but runtime types are the same.

@EgorBo
Copy link
Member

EgorBo commented Jul 14, 2022

typeof(delegate* unmanaged<float, void*, void>)

I am not sure I get this one, aren't they the same like all function pointers?
@jakobbotsch pointed me that in your case it's folded to false instead of true

image

@EgorBo
Copy link
Member

EgorBo commented Jul 14, 2022

I'd check that it does not break any of these:

using System.Runtime.CompilerServices;


public enum Enum1 : int { A }
public enum Enum2 : uint { A }

class Program
{
    static void Main()
    {
        // false
        Console.WriteLine(Test<int, Enum1>());
        Console.WriteLine(Test<Enum1, Enum2>());
        Console.WriteLine(Test<int?, uint?>());
        Console.WriteLine(Test<int?, Enum1?>());
        Console.WriteLine(Test<ValueTuple<Program>, ValueTuple<string>>()); // ValueTuple<_Canon> vs ValueTuple<_Canon>

        // true
        Console.WriteLine(Test<Program, Program>());
        Console.WriteLine(Test<ValueTuple<Program>, ValueTuple<Program>>());
    }


    [MethodImpl(MethodImplOptions.NoInlining)]
    static bool Test<T1, T2>()
    {
        Type t = typeof(T1);
        Console.WriteLine();
        return t == typeof(T2);
    }
}

@jakobbotsch
Copy link
Member

jakobbotsch commented Jul 14, 2022

Seems like the jit interface methods canInlineTypeCheck and compareTypesForEquality both might need updating, since type handles may be distinct but runtime types are the same.

I guess the problem might be more on the JIT side in assuming injectivity of this helper again. It makes sense to me that the VM says that those two type handles are not equal, and there may be other cases in the JIT where that is the desired behavior. Unfortunately that means we may need a new JIT-EE method for this purpose...

Although it looks like this function pointers are the only case of this, so maybe we can do something simple.

@AndyAyersMS
Copy link
Member Author

I'd check that it does not break any of these:

The only broken ones I've found so far are function pointers.

Seems like the jit interface methods canInlineTypeCheck and compareTypesForEquality both might need updating, since type handles may be distinct but runtime types are the same.

I guess the problem might be more on the JIT side in assuming injectivity of this helper again. It makes sense to me that the VM says that those two type handles are not equal, and there may be other cases in the JIT where that is the desired behavior. Unfortunately that means we may need a new JIT-EE method for this purpose...

Although it looks like this function pointers are the only case of this, so maybe we can do something simple

(ignoring this PR for the moment) the jit only folds to class handle compares if the VM says it's ok to do so. So still think the fix needs to be done there...

Also not sure what the rules are, for instance

typeof(delegate*<int>[]) == typeof(delegate*<float>[]);

seems to be false, so seems like we can just do a superficial check and not have to dig in.

For this PR we need VN to call that same helper, and can only optimize the cases where we have known class handles.

I still need to check the GetType behavior.

@AndyAyersMS
Copy link
Member Author

Not sure what the right fix is. We don't want to abandon the general idea that class handles uniquely map to types. But by the time the jit sees these types they're no longer function pointers, they are mapped to variants of UIntPtr.

Added a test case showing that we mis-optimize some cases (True0, False16) involving function pointer types when we optimize (even without the VN aspect added here).

@AndyAyersMS
Copy link
Member Author

See also #11354

@markples
Copy link
Member

nits - as in the comments above, they are mapped to IntPtr, not UIntPtr, and they seem to be mapped to the exact same Type, not variants of it (both of these points can be seen in @EgorBo's example above)

This reminds me of the old stuff where int vs uint and int vs MyEnumBasedOnInt were sometimes the same and sometimes different depending on isinst vs castclass vs unbox vs array cast vs probably more. I don't know if that's still the case, though I'm guessing yes given Egor's other example above. We had to have multiple sets of type rules depending on what question was being asked. (Indeed, if you construct an array of one of these delegate types and check if it is an array of the other, then they are kept separate.)

We don't want to abandon the general idea that class handles uniquely map to types.

This sounds scary since this JIT doesn't own the mapping. But I guess if it's close enough (as suggested above, maybe it's only function pointers?), then the logic ("uniquely map unless function pointers") could be safely duplicated in the JIT, perhaps with an assertion/test/something to catch it if it changes in the future.

I wonder if there are other corner cases though. Maybe specifying int vs System.Int32 in the encoding (or other weird stuff with TypeSpec encodings). Or int[0..5] vs int[2..5], which (if I recall correctly and hasn't changed from long ago) could be written in the IL types but didn't actually have semantic meaning.

@AndyAyersMS
Copy link
Member Author

But by the time the jit sees these types they're no longer function pointers

This turned out to be operator error; I was modifying the runtime but then only rebuilding the jit. They are indeed function pointer type descs.

nits - as in the comments above, they are mapped to IntPtr, not UIntPtr, and they seem to be mapped to the exact same Type, not variants of it (both of these points can be seen in @EgorBo's example above)

Right -- but for some reason when the jit asks the runtime for the name of the type it gets back UIntPtr.

At any rate, since the handles are FnPtrTypeDesc we can check for them on the runtime side and tell the jit to avoid optimizing. Annoyingly we've removed the helper we were once able to call so we just have to bail out entirely if either operand is a function pointer.

Presumably something similar will be needed for the jit interface methods over in crossgen2.

@markples
Copy link
Member

Right -- but for some reason when the jit asks the runtime for the name of the type it gets back UIntPtr.

Fascinating. I wonder if this is tied to the "evaluation stack" view of the world, where signed vs unsigned doesn't exist until you get to how specific instructions interpret the bits. (I, 12.3.2.1)

@AndyAyersMS
Copy link
Member Author

As Jan noted elsewhere, Mono doesn't do things the same way.

I'm also seeing some arm64 failures on coreclr that will need investigating.

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

looks like the jit IR holds onto the "compile time handle" but VN doesn't know how to get to this from the embedded handle. Think I can just build a map.

Or you can add JIT/EE interface method for this. The EE side has to have this mapping already.

@AndyAyersMS
Copy link
Member Author

looks like the jit IR holds onto the "compile time handle" but VN doesn't know how to get to this from the embedded handle. Think I can just build a map.

Or you can add JIT/EE interface method for this. The EE side has to have this mapping already.

I created a map on the jit side for VN for now.

There are some cases we will need to look at further, where the jit creates method table constants (eg via GDV) from "thin air" and so we have embedded handles with no associated compile time handle. Seems like to make this work for AOT we'll want to use the EE side map to figure out the right representation.

@AndyAyersMS
Copy link
Member Author

AndyAyersMS commented Jul 19, 2022

Mono LLVM AOT failing with

2022-07-19T05:19:59.0654741Z   Mono Ahead of Time compiler - compiling assembly /__w/1/s/artifacts/tests/coreclr/Linux.x64.Release/JIT/opt/ValueNumbering/TypeTestFolding/TypeTestFolding.dll
2022-07-19T05:19:59.0658598Z   AOTID FB62C47E-1970-8E09-3D6A-5C9BF58530B7
2022-07-19T05:19:59.0660732Z   * Assertion at /__w/1/s/src/mono/mono/mini/aot-compiler.c:3409, condition `m_class_get_rank (klass) > 0' not met

Opened #72460, will exclude for now.

@AndyAyersMS
Copy link
Member Author

AndyAyersMS commented Jul 19, 2022

Installer failure is some kind of contention/timing issue

##[error].packages\microsoft.dotnet.arcade.sdk\7.0.0-beta.22362.1\tools\VSTest.targets(55,5): error MSB3491: (NETCORE_ENGINEERING_TELEMETRY=Build) Could not write lines to file "D:\a\_work\1\s\artifacts\log\Release\Microsoft.NET.HostModel.ComHost.Tests_net7.0_x64.log". The process cannot access the file 'D:\a\_work\1\s\artifacts\log\Release\Microsoft.NET.HostModel.ComHost.Tests_net7.0_x64.log' because it is being used by another process.

Libraries test failure is #72429

SPMI shows about a 0.5% TP impact which is quite surprising. Going to investigate.

@AndyAyersMS
Copy link
Member Author

I can reduce TP impact to around 0.1% with a bit of refactoring, and by revising the various VNPairForFunc to check for the case where liberal and conservative VNs match.

Latter seems like it should be its own PR, so will just push the refactoring bit here.

@AndyAyersMS
Copy link
Member Author

TP down to 0.2% but now, no SPMI diffs. Hmm.

@AndyAyersMS
Copy link
Member Author

Still seeing mono failures despite #72479. cc @vargaz

Unhandled Exception:
System.ExecutionEngineException: Attempting to JIT compile method 'int TypeTestFolding:Main ()' while running in aot-only mode. See https://docs.microsoft.com/xamarin/ios/internals/limitations for more information.

[ERROR] FATAL UNHANDLED EXCEPTION: System.ExecutionEngineException: Attempting to JIT compile method 'int TypeTestFolding:Main ()' while running in aot-only mode. See https://docs.microsoft.com/xamarin/ios/internals/limitations for more information.

TP is now 0.2% - 0.3%, still seems high for something that rarely kicks in. #72527 will help offset this somewhat.

@AndyAyersMS
Copy link
Member Author

@jakobbotsch PTAL

Copy link
Member

@jakobbotsch jakobbotsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Is it expected that we see no SPMI diffs at all?

@AndyAyersMS
Copy link
Member Author

LGTM. Is it expected that we see no SPMI diffs at all?

Good question. There are diffs in the newly added test case, and there were SPMI diffs in earlier versions. It's possible that the SPMI queries made here now fail so the methods with diffs are failing in SPMI.

Let me try running PMI diffs locally.

@AndyAyersMS
Copy link
Member Author

PMI diffs

PMI CodeSize Diffs for System.Private.CoreLib.dll, framework assemblies for x64 default jit

Summary of Code Size diffs:
(Lower is better)

Total bytes of base: 58704844
Total bytes of diff: 58701277
Total bytes of delta: -3567 (-0.01 % of base)
Total relative delta: -5.85
    diff is an improvement.
    relative diff is an improvement.


Top file regressions (bytes):
          51 : System.ComponentModel.TypeConverter.dasm (0.02% of base)

Top file improvements (bytes):
       -3358 : Microsoft.CodeAnalysis.VisualBasic.dasm (-0.05% of base)
        -147 : System.Diagnostics.DiagnosticSource.dasm (-0.08% of base)
        -107 : System.Composition.TypedParts.dasm (-0.20% of base)
          -6 : System.Private.CoreLib.dasm (-0.00% of base)

5 total files with Code Size differences (4 improved, 1 regressed), 268 unchanged.

Top method regressions (bytes):
          51 ( 8.44% of base) : System.ComponentModel.TypeConverter.dasm - Timer:set_Enabled(bool):this

Top method improvements (bytes):
       -1103 (-94.43% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):Nullable`1
        -912 (-95.40% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):double
        -538 (-95.56% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):long
        -387 (-93.93% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):int
        -247 (-90.81% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):short
        -171 (-87.24% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):ubyte
        -147 (-26.78% of base) : System.Diagnostics.DiagnosticSource.dasm - Instrument:ValidateTypeParameter() (7 methods)
        -107 (-7.45% of base) : System.Composition.TypedParts.dasm - OnImportsSatisfiedFeature:RewriteActivator(TypeInfo,CompositeActivator,IDictionary`2,IEnumerable`1):CompositeActivator:this
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):double
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):int
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):long
          -1 (-0.29% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):Nullable`1
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):short
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):ubyte

Top method regressions (percentages):
          51 ( 8.44% of base) : System.ComponentModel.TypeConverter.dasm - Timer:set_Enabled(bool):this

Top method improvements (percentages):
        -538 (-95.56% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):long
        -912 (-95.40% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):double
       -1103 (-94.43% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):Nullable`1
        -387 (-93.93% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):int
        -247 (-90.81% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):short
        -171 (-87.24% of base) : Microsoft.CodeAnalysis.VisualBasic.dasm - Conversions:ToGenericParameter(Object):ubyte
        -147 (-26.78% of base) : System.Diagnostics.DiagnosticSource.dasm - Instrument:ValidateTypeParameter() (7 methods)
        -107 (-7.45% of base) : System.Composition.TypedParts.dasm - OnImportsSatisfiedFeature:RewriteActivator(TypeInfo,CompositeActivator,IDictionary`2,IEnumerable`1):CompositeActivator:this
          -1 (-0.29% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):Nullable`1
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):int
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):long
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):ubyte
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):short
          -1 (-0.28% of base) : System.Private.CoreLib.dasm - Marshal:GetDelegateForFunctionPointer(long):double

15 total methods with Code Size differences (14 improved, 1 regressed), 254456 unchanged

@jakobbotsch
Copy link
Member

PMI diffs

Thanks.

This seems like the kind of pattern that could be very prevalent in user code, so even with a low amount of hits in our code it seems reasonable to me to do it.

Good example of a case where having a way to get diffs for user code would probably be beneficial.

@AndyAyersMS AndyAyersMS merged commit 91d15ff into dotnet:main Jul 25, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 24, 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 this pull request may close these issues.

JIT: typeof() is not propagated/folded
6 participants