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

Import isinst/castclass Nullable as underlying type #87241

Merged
merged 13 commits into from Jun 21, 2023

Conversation

huoyaoyuan
Copy link
Member

According to ECMA-335 III.4.6:

If typeTok is a non-nullable value type or a generic parameter type it is interpreted
as “boxed” typeTok. If typeTok is a nullable type, Nullable<T>, it is interpreted as
“boxed” T.

In my understanding, it means that isinst V? is the same as isinst V.

Code gen:

        bool TestNullable(object o) => o is int?;

        int TestCoalesce(object o) => (o as int?) ?? 1234;

Before:

; Method CSPlayground.Program:TestNullable(System.Object):bool:this
G_M50267_IG01:  ;; offset=0000H
       sub      rsp, 40
						;; size=4 bbWeight=1 PerfScore 0.25

G_M50267_IG02:  ;; offset=0004H
       mov      rcx, 0x7FF8A0D4A9B8      ; System.Nullable`1[int]
       call     [CORINFO_HELP_ISINSTANCEOFANY]
       test     rax, rax
       setne    al
       movzx    rax, al
						;; size=25 bbWeight=1 PerfScore 4.75

G_M50267_IG03:  ;; offset=001DH
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 34

; Method CSPlayground.Program:TestCoalesce(System.Object):int:this
G_M24774_IG01:  ;; offset=0000H
       sub      rsp, 40
       xor      eax, eax
       mov      qword ptr [rsp+20H], rax
						;; size=11 bbWeight=1 PerfScore 1.50

G_M24774_IG02:  ;; offset=000BH
       mov      rcx, 0x7FF8A0D1A9B8      ; System.Nullable`1[int]
       call     [CORINFO_HELP_ISINSTANCEOFANY]
       mov      r8, rax
       lea      rcx, [rsp+20H]
       mov      rdx, 0x7FF8A0D1A9B8      ; System.Nullable`1[int]
       call     CORINFO_HELP_UNBOX_NULLABLE
       mov      eax, 0x4D2
       cmp      byte  ptr [rsp+20H], 0
       cmovne   eax, dword ptr [rsp+24H]
						;; size=54 bbWeight=1 PerfScore 9.50

G_M24774_IG03:  ;; offset=0041H
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 70

After:

; Method CSPlayground.Program:TestNullable(System.Object):bool:this
G_M50267_IG01:  ;; offset=0000H
						;; size=0 bbWeight=1 PerfScore 0.00

G_M50267_IG02:  ;; offset=0000H
       test     rdx, rdx
       je       SHORT G_M50267_IG04
						;; size=5 bbWeight=1 PerfScore 1.25

G_M50267_IG03:  ;; offset=0005H
       mov      rax, 0x7FF89421E7E8      ; System.Int32
       xor      rcx, rcx
       cmp      qword ptr [rdx], rax
       cmovne   rdx, rcx
						;; size=19 bbWeight=0.25 PerfScore 0.94

G_M50267_IG04:  ;; offset=0018H
       xor      eax, eax
       test     rdx, rdx
       setne    al
						;; size=8 bbWeight=1 PerfScore 1.50

G_M50267_IG05:  ;; offset=0020H
       ret      
						;; size=1 bbWeight=1 PerfScore 1.00
; Total bytes of code: 33

; Method CSPlayground.Program:TestCoalesce(System.Object):int:this
G_M24774_IG01:  ;; offset=0000H
       sub      rsp, 40
       xor      eax, eax
       mov      qword ptr [rsp+20H], rax
						;; size=11 bbWeight=1 PerfScore 1.50

G_M24774_IG02:  ;; offset=000BH
       mov      r8, rdx
       test     r8, r8
       je       SHORT G_M24774_IG04
						;; size=8 bbWeight=1 PerfScore 1.50

G_M24774_IG03:  ;; offset=0013H
       mov      rcx, 0x7FF89420E7E8      ; System.Int32
       xor      rdx, rdx
       cmp      qword ptr [r8], rcx
       cmovne   r8, rdx
						;; size=19 bbWeight=0.25 PerfScore 0.94

G_M24774_IG04:  ;; offset=0026H
       lea      rcx, [rsp+20H]
       mov      rdx, 0x7FF89451A9B8      ; System.Nullable`1[int]
       call     CORINFO_HELP_UNBOX_NULLABLE
       mov      eax, 0x4D2
       cmp      byte  ptr [rsp+20H], 0
       cmovne   eax, dword ptr [rsp+24H]
						;; size=35 bbWeight=1 PerfScore 6.00

G_M24774_IG05:  ;; offset=0049H
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 78

Still not ideal (#47920 (comment)), but reduces a helper call.

@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 Jun 8, 2023
@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jun 8, 2023
@ghost
Copy link

ghost commented Jun 8, 2023

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

Issue Details

According to ECMA-335 III.4.6:

If typeTok is a non-nullable value type or a generic parameter type it is interpreted
as “boxed” typeTok. If typeTok is a nullable type, Nullable<T>, it is interpreted as
“boxed” T.

In my understanding, it means that isinst V? is the same as isinst V.

Code gen:

        bool TestNullable(object o) => o is int?;

        int TestCoalesce(object o) => (o as int?) ?? 1234;

Before:

; Method CSPlayground.Program:TestNullable(System.Object):bool:this
G_M50267_IG01:  ;; offset=0000H
       sub      rsp, 40
						;; size=4 bbWeight=1 PerfScore 0.25

G_M50267_IG02:  ;; offset=0004H
       mov      rcx, 0x7FF8A0D4A9B8      ; System.Nullable`1[int]
       call     [CORINFO_HELP_ISINSTANCEOFANY]
       test     rax, rax
       setne    al
       movzx    rax, al
						;; size=25 bbWeight=1 PerfScore 4.75

G_M50267_IG03:  ;; offset=001DH
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 34

; Method CSPlayground.Program:TestCoalesce(System.Object):int:this
G_M24774_IG01:  ;; offset=0000H
       sub      rsp, 40
       xor      eax, eax
       mov      qword ptr [rsp+20H], rax
						;; size=11 bbWeight=1 PerfScore 1.50

G_M24774_IG02:  ;; offset=000BH
       mov      rcx, 0x7FF8A0D1A9B8      ; System.Nullable`1[int]
       call     [CORINFO_HELP_ISINSTANCEOFANY]
       mov      r8, rax
       lea      rcx, [rsp+20H]
       mov      rdx, 0x7FF8A0D1A9B8      ; System.Nullable`1[int]
       call     CORINFO_HELP_UNBOX_NULLABLE
       mov      eax, 0x4D2
       cmp      byte  ptr [rsp+20H], 0
       cmovne   eax, dword ptr [rsp+24H]
						;; size=54 bbWeight=1 PerfScore 9.50

G_M24774_IG03:  ;; offset=0041H
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 70

After:

; Method CSPlayground.Program:TestNullable(System.Object):bool:this
G_M50267_IG01:  ;; offset=0000H
						;; size=0 bbWeight=1 PerfScore 0.00

G_M50267_IG02:  ;; offset=0000H
       test     rdx, rdx
       je       SHORT G_M50267_IG04
						;; size=5 bbWeight=1 PerfScore 1.25

G_M50267_IG03:  ;; offset=0005H
       mov      rax, 0x7FF89421E7E8      ; System.Int32
       xor      rcx, rcx
       cmp      qword ptr [rdx], rax
       cmovne   rdx, rcx
						;; size=19 bbWeight=0.25 PerfScore 0.94

G_M50267_IG04:  ;; offset=0018H
       xor      eax, eax
       test     rdx, rdx
       setne    al
						;; size=8 bbWeight=1 PerfScore 1.50

G_M50267_IG05:  ;; offset=0020H
       ret      
						;; size=1 bbWeight=1 PerfScore 1.00
; Total bytes of code: 33

; Method CSPlayground.Program:TestCoalesce(System.Object):int:this
G_M24774_IG01:  ;; offset=0000H
       sub      rsp, 40
       xor      eax, eax
       mov      qword ptr [rsp+20H], rax
						;; size=11 bbWeight=1 PerfScore 1.50

G_M24774_IG02:  ;; offset=000BH
       mov      r8, rdx
       test     r8, r8
       je       SHORT G_M24774_IG04
						;; size=8 bbWeight=1 PerfScore 1.50

G_M24774_IG03:  ;; offset=0013H
       mov      rcx, 0x7FF89420E7E8      ; System.Int32
       xor      rdx, rdx
       cmp      qword ptr [r8], rcx
       cmovne   r8, rdx
						;; size=19 bbWeight=0.25 PerfScore 0.94

G_M24774_IG04:  ;; offset=0026H
       lea      rcx, [rsp+20H]
       mov      rdx, 0x7FF89451A9B8      ; System.Nullable`1[int]
       call     CORINFO_HELP_UNBOX_NULLABLE
       mov      eax, 0x4D2
       cmp      byte  ptr [rsp+20H], 0
       cmovne   eax, dword ptr [rsp+24H]
						;; size=35 bbWeight=1 PerfScore 6.00

G_M24774_IG05:  ;; offset=0049H
       add      rsp, 40
       ret      
						;; size=5 bbWeight=1 PerfScore 1.25
; Total bytes of code: 78

Still not ideal (#47920 (comment)), but reduces a helper call.

Author: huoyaoyuan
Assignees: -
Labels:

area-CodeGen-coreclr, community-contribution

Milestone: -

Comment on lines 5993 to 5996
if (!clsHnd.IsTypeDesc() && !(Nullable::IsNullableType(clsHnd) && fThrowing))
{
// If it is a non-variant class, use the fast class helper
// Also use fast helper for isinst Nullable, which is equal to isinst of underlying type
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'm not sure where to handle this. JIT is passing the token of original nullable type. Is there any way to get the token of underlying type?

Copy link
Member

Choose a reason for hiding this comment

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

Is there any way to get the token of underlying type?

Do you mean to get int from Nullable<int> ? in that case it should be clsHnd->AsMethodTable()->GetInstantiation()[0]

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 mean to get it in JIT instead of VM.

I didn't update jitinterface for native aot (if exists). If done in JIT, there's no need to update in two places.

Copy link
Member

Choose a reason for hiding this comment

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

um.. getTypeForBox that you already using?

Copy link
Member Author

Choose a reason for hiding this comment

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

It returns a VM handle, but the JIT interface requires a metadata handle, if I'm correct.
Or we can skip the call to JIT interface and hard code to exact type helper when nullable case detected. Is there any risk?

Copy link
Member

@EgorBo EgorBo Jun 8, 2023

Choose a reason for hiding this comment

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

Presumably, you need to check that you don't deal with Nullable<MyValueType<_Canon>> (or emit a runtime type lookup)

Copy link
Member

Choose a reason for hiding this comment

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

public class Program
{
    public static void Main()
    {
        Test<string>(new MyStruct<string>());
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static bool Test<T>(object o) => o is MyStruct<T>?;
}

public struct MyStruct<T>
{
    public T val;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Solutions I can see:

  • Call getCastingHelperStatic again after getCastingHelper. Is the call to classMustBeLoadedBeforeCodeIsRun reliable to mark for underlying type too?
  • Find a way to convert CORINFO_RESOLVED_TOKEN to underlying type
  • Update JIT interface and tell getCastingHelper to use underlying type.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since the implementation of getCastingHelper are all converting it to vm handle immediately, I think it's better to update jit interface to accept CORINFO_CLASS_HANDLE.

@huoyaoyuan
Copy link
Member Author

huoyaoyuan commented Jun 8, 2023

Oops, forgot the null check difference. Converting to draft until I handle it.

Null check difference is for as unboxing. null is int? is false.

@huoyaoyuan huoyaoyuan marked this pull request as draft June 8, 2023 01:52
@huoyaoyuan huoyaoyuan marked this pull request as ready for review June 8, 2023 01:53
@EgorBo
Copy link
Member

EgorBo commented Jun 8, 2023

CI failures look related?

@EgorBo
Copy link
Member

EgorBo commented Jun 8, 2023

The codegen for as looks a bit weird to me. Basically, it is expected to be:

if (obj != null && obj->pMT == int32)
    return Nullable<int> { hasValue = true, Value = unboxedValu }
else
    fallback

@@ -5710,7 +5720,7 @@ GenTree* Compiler::impCastClassOrIsInstToTree(
assert(lclDsc->lvSingleDef == 0);
lclDsc->lvSingleDef = 1;
JITDUMP("Marked V%02u as a single def temp\n", tmp);
lvaSetClass(tmp, pResolvedToken->hClass);
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there any observable effect for changing the return type of the expression?

@@ -5422,9 +5422,19 @@ GenTree* Compiler::impCastClassOrIsInstToTree(
{
assert(op1->TypeGet() == TYP_REF);

// Import isinst Nullable<V> as isinst V
Copy link
Member

Choose a reason for hiding this comment

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

If all you want to do is to convert Nullable<T> into T for casing helpers, the best place to do it is resolveToken on the VM side. resolveToken on VM side does similar conversion for NewArr here: https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/jitinterface.cpp#L1170. You can do similar conversion for tokens that come from isinst/castclass.

Copy link
Member

Choose a reason for hiding this comment

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

The conversion has to be also taken into account when embedding the handle (look for other places that check for CORINFO_TOKENKIND_Newarr in the VM). I think your current change is missing this part.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's null check difference for castclass, so the nullable type should be visible to JIT if we want to do optimization for castclass.

Copy link
Member

Choose a reason for hiding this comment

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

There's null check difference for castclass

Could you please elaborate? Both isinst on null and castclass on null return null.

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 misunderstood. NRE for casting to value type is thrown by unbox.

@@ -1170,6 +1170,16 @@ void CEEInfo::resolveToken(/* IN, OUT */ CORINFO_RESOLVED_TOKEN * pResolvedToken
th = ClassLoader::LoadArrayTypeThrowing(th);
break;

case CORINFO_TOKENKIND_Casting:
// Disallow ELEMENT_TYPE_BYREF and ELEMENT_TYPE_VOID
Copy link
Member

Choose a reason for hiding this comment

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

This is duplicated for 3rd time here. We should move it in front of the switch under if (tokenType != CORINFO_TOKENKIND_Ldtoken)

@huoyaoyuan
Copy link
Member Author

It seems that roslyn doesn't emit castclass for value type. The following C++/CLI emits castclass:

		int^ Test(Object^ o)
		{
			return safe_cast<int^>(o);
		}
.method public hidebysig 
	instance class [mscorlib]System.ValueType modopt([mscorlib]System.Int32) modopt([mscorlib]System.Runtime.CompilerServices.IsBoxed) Test (
		object o
	) cil managed 
{
	// Method begins at RVA 0x10e5c
	// Header size: 12
	// Code size: 7 (0x7)
	.maxstack 1

	IL_0000: ldarg.1
	IL_0001: castclass [mscorlib]System.Int32
	IL_0006: ret
} // end of method Class1::Test

It's quite complicated to verify it since /clr:netcore is not working for me. Do we have IL coverage in jit test?

@EgorBo
Copy link
Member

EgorBo commented Jun 9, 2023

Do we have IL coverage in jit test?

yes, look for *.ilproj in the tests folder

@huoyaoyuan
Copy link
Member Author

yes, look for *.ilproj in the tests folder

Yes I know that folder. It seems there's currently no cases covering castclass valuetype. Where could such tests be put into? Directed/Convert? IL_Conformance?

COMPlusThrow(kInvalidProgramException);
break;
default:
// No additional checks. Satisfy switch exhaustiveness check.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// No additional checks. Satisfy switch exhaustiveness check.

Nit. I do not think this comment is useful.

@jkotas
Copy link
Member

jkotas commented Jun 10, 2023

Where could such tests be put into? Directed/Convert? IL_Conformance?

JIT\opt\Casts may be a good place.

@jkotas
Copy link
Member

jkotas commented Jun 15, 2023

use pRuntimeLookup in impTokenToHandle to determine whether it's a generic parameter.

I think you would want to check CORINFO_FLG_SHAREDINST flag instead, before impTokenToHandle is called.

@jkotas
Copy link
Member

jkotas commented Jun 16, 2023

Would this have other side effects?

I do not see problems with this, as long as you avoid using the original RESOLVED_TOKEN after updating the class handle.

@jkotas
Copy link
Member

jkotas commented Jun 16, 2023

/azp run runtime-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jkotas
Copy link
Member

jkotas commented Jun 16, 2023

/azp run runtime-extra-platforms

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jkotas
Copy link
Member

jkotas commented Jun 17, 2023

/azp run runtime-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@huoyaoyuan
Copy link
Member Author

Test failure should be the known one.

@@ -5425,6 +5425,24 @@ GenTree* Compiler::impCastClassOrIsInstToTree(
// Optimistically assume the jit should expand this as an inline test
bool shouldExpandInline = true;
bool isClassExact = impIsClassExact(pResolvedToken->hClass);
bool isSharedInst = info.compCompHnd->getClassAttribs(pResolvedToken->hClass) & CORINFO_FLG_SHAREDINST;
Copy link
Member

Choose a reason for hiding this comment

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

We can avoid the JIT/EE interface call for !isClassExact case by inlining the JIT/EE interface into the condition below.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM otherwise. @dotnet/jit-contrib Any addition feedback?

{
.method public hidebysig static int32 Main() cil managed
{
.custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = (
Copy link
Member

Choose a reason for hiding this comment

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

I assume the [Fact] attribute is not needed here (or then .entrypoint is not needed if it's part of the merged runtime tests)?

I'm not asking to remove it, just curious

Copy link
Member

Choose a reason for hiding this comment

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

[Fact] is needed as JIT/opt is a merged test group. The .entrypoint will have no impact on merged test group execution (this test is a library that is referenced by the merged test group, so this .entrypoint isn't the overall entry).

We've been keeping the .entrypoint annotations in ilproj tests. I'm not entirely sure of the details, but my guess is that the basic wrappers (for BuildAsStandalone or RequiresProcessIsolation) have C#-specific aspects to them.

Copy link
Member

Choose a reason for hiding this comment

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

@markples I see, thanks, I thought that .entrypoint will confuse a merged test group

@EgorBo
Copy link
Member

EgorBo commented Jun 19, 2023

LGTM assuming the newly added outerloop test passed during manual azp run outerloop checks

I've restarted the failing CI job

@huoyaoyuan
Copy link
Member Author

@jkotas anything remaining?

@jkotas jkotas merged commit 74196a0 into dotnet:main Jun 21, 2023
138 checks passed
@jkotas
Copy link
Member

jkotas commented Jun 21, 2023

Thank you!

@huoyaoyuan huoyaoyuan deleted the isinst-nullable branch June 21, 2023 04:40
@dotnet dotnet locked as resolved and limited conversation to collaborators Jul 21, 2023
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 community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants