Skip to content

Import isinst/castclass Nullable as underlying type - #87241

Merged
jkotas merged 13 commits into
dotnet:mainfrom
huoyaoyuan:isinst-nullable
Jun 21, 2023
Merged

Import isinst/castclass Nullable as underlying type#87241
jkotas merged 13 commits into
dotnet:mainfrom
huoyaoyuan:isinst-nullable

Conversation

@huoyaoyuan

Copy link
Copy Markdown
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:

boolTestNullable(objecto)=>oisint?;intTestCoalesce(objecto)=>(oasint?)??1234;

Before:

; Method CSPlayground.Program:TestNullable(System.Object):bool:thisG_M50267_IG01: ;; offset=0000Hsubrsp,40 ;; size=4 bbWeight=1 PerfScore 0.25G_M50267_IG02: ;; offset=0004Hmovrcx,0x7FF8A0D4A9B8 ; System.Nullable`1[int]call[CORINFO_HELP_ISINSTANCEOFANY]testrax,rax setne almovzxrax,al ;; size=25 bbWeight=1 PerfScore 4.75G_M50267_IG03: ;; offset=001DHaddrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25; Total bytes of code: 34; Method CSPlayground.Program:TestCoalesce(System.Object):int:thisG_M24774_IG01: ;; offset=0000Hsubrsp,40xoreax,eaxmov qword ptr [rsp+20H],rax ;; size=11 bbWeight=1 PerfScore 1.50G_M24774_IG02: ;; offset=000BHmovrcx,0x7FF8A0D1A9B8 ; System.Nullable`1[int]call[CORINFO_HELP_ISINSTANCEOFANY]movr8,raxlearcx,[rsp+20H]movrdx,0x7FF8A0D1A9B8 ; System.Nullable`1[int]call CORINFO_HELP_UNBOX_NULLABLEmoveax,0x4D2cmp byte ptr [rsp+20H],0 cmovne eax, dword ptr [rsp+24H] ;; size=54 bbWeight=1 PerfScore 9.50G_M24774_IG03: ;; offset=0041Haddrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25; Total bytes of code: 70

After:

; Method CSPlayground.Program:TestNullable(System.Object):bool:thisG_M50267_IG01: ;; offset=0000H ;; size=0 bbWeight=1 PerfScore 0.00G_M50267_IG02: ;; offset=0000Htestrdx,rdxje SHORT G_M50267_IG04 ;; size=5 bbWeight=1 PerfScore 1.25G_M50267_IG03: ;; offset=0005Hmovrax,0x7FF89421E7E8 ; System.Int32xorrcx,rcxcmp qword ptr [rdx],rax cmovne rdx,rcx ;; size=19 bbWeight=0.25 PerfScore 0.94G_M50267_IG04: ;; offset=0018Hxoreax,eaxtestrdx,rdx setne al ;; size=8 bbWeight=1 PerfScore 1.50G_M50267_IG05: ;; offset=0020Hret ;; size=1 bbWeight=1 PerfScore 1.00; Total bytes of code: 33; Method CSPlayground.Program:TestCoalesce(System.Object):int:thisG_M24774_IG01: ;; offset=0000Hsubrsp,40xoreax,eaxmov qword ptr [rsp+20H],rax ;; size=11 bbWeight=1 PerfScore 1.50G_M24774_IG02: ;; offset=000BHmovr8,rdxtestr8,r8je SHORT G_M24774_IG04 ;; size=8 bbWeight=1 PerfScore 1.50G_M24774_IG03: ;; offset=0013Hmovrcx,0x7FF89420E7E8 ; System.Int32xorrdx,rdxcmp qword ptr [r8],rcx cmovne r8,rdx ;; size=19 bbWeight=0.25 PerfScore 0.94G_M24774_IG04: ;; offset=0026Hlearcx,[rsp+20H]movrdx,0x7FF89451A9B8 ; System.Nullable`1[int]call CORINFO_HELP_UNBOX_NULLABLEmoveax,0x4D2cmp byte ptr [rsp+20H],0 cmovne eax, dword ptr [rsp+24H] ;; size=35 bbWeight=1 PerfScore 6.00G_M24774_IG05: ;; offset=0049Haddrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25; Total bytes of code: 78

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

@ghostghost added 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 labels Jun 8, 2023
@ghost

ghost commented Jun 8, 2023

Copy link
Copy Markdown

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:

boolTestNullable(objecto)=>oisint?;intTestCoalesce(objecto)=>(oasint?)??1234;

Before:

; Method CSPlayground.Program:TestNullable(System.Object):bool:thisG_M50267_IG01: ;; offset=0000Hsubrsp,40 ;; size=4 bbWeight=1 PerfScore 0.25G_M50267_IG02: ;; offset=0004Hmovrcx,0x7FF8A0D4A9B8 ; System.Nullable`1[int]call[CORINFO_HELP_ISINSTANCEOFANY]testrax,rax setne almovzxrax,al ;; size=25 bbWeight=1 PerfScore 4.75G_M50267_IG03: ;; offset=001DHaddrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25; Total bytes of code: 34; Method CSPlayground.Program:TestCoalesce(System.Object):int:thisG_M24774_IG01: ;; offset=0000Hsubrsp,40xoreax,eaxmov qword ptr [rsp+20H],rax ;; size=11 bbWeight=1 PerfScore 1.50G_M24774_IG02: ;; offset=000BHmovrcx,0x7FF8A0D1A9B8 ; System.Nullable`1[int]call[CORINFO_HELP_ISINSTANCEOFANY]movr8,raxlearcx,[rsp+20H]movrdx,0x7FF8A0D1A9B8 ; System.Nullable`1[int]call CORINFO_HELP_UNBOX_NULLABLEmoveax,0x4D2cmp byte ptr [rsp+20H],0 cmovne eax, dword ptr [rsp+24H] ;; size=54 bbWeight=1 PerfScore 9.50G_M24774_IG03: ;; offset=0041Haddrsp,40ret ;; size=5 bbWeight=1 PerfScore 1.25; Total bytes of code: 70

After:

; Method CSPlayground.Program:TestNullable(System.Object):bool:thisG_M50267_IG01: ;; offset=0000H ;; size=0 bbWeight=1 PerfScore 0.00G_M50267_IG02: ;; offset=0000Htestrdx,rdxje SHORT G_M50267_IG04 ;; size=5 bbWeight=1 PerfScore 1.25G_M50267_IG03: ;; offset=0005Hmovrax,0x7FF89421E7E8 ; System.Int32xorrcx,rcxcmp qword ptr [rdx],rax cmovne rdx,rcx ;; size=19 bbWeight=0.25 PerfScore 0.94G_M50267_IG04: ;; offset=0018Hxoreax,eaxtestrdx,rdx setne al ;; size=8 bbWeight=1 PerfScore 1.50G_M50267_IG05: ;; offset=0020Hret ;; size=1 bbWeight=1 PerfScore 1.00; Total bytes of code: 33; Method CSPlayground.Program:TestCoalesce(System.Object):int:thisG_M24774_IG01: ;; offset=0000Hsubrsp,40xoreax,eaxmov qword ptr [rsp+20H],rax ;; size=11 bbWeight=1 PerfScore 1.50G_M24774_IG02: ;; offset=000BHmovr8,rdxtestr8,r8je SHORT G_M24774_IG04 ;; size=8 bbWeight=1 PerfScore 1.50G_M24774_IG03: ;; offset=0013Hmovrcx,0x7FF89420E7E8 ; System.Int32xorrdx,rdxcmp qword ptr [r8],rcx cmovne r8,rdx ;; size=19 bbWeight=0.25 PerfScore 0.94G_M24774_IG04: ;; offset=0026Hlearcx,[rsp+20H]movrdx,0x7FF89451A9B8 ; System.Nullable`1[int]call CORINFO_HELP_UNBOX_NULLABLEmoveax,0x4D2cmp byte ptr [rsp+20H],0 cmovne eax, dword ptr [rsp+24H] ;; size=35 bbWeight=1 PerfScore 6.00G_M24774_IG05: ;; offset=0049Haddrsp,40ret ;; 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 threadsrc/coreclr/vm/jitinterface.cpp Outdated
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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?

@EgorBoEgorBoJun 8, 2023

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

Choose a reason for hiding this comment

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

publicclassProgram{publicstaticvoidMain(){Test<string>(newMyStruct<string>());}[MethodImpl(MethodImplOptions.NoInlining)]staticboolTest<T>(objecto)=>oisMyStruct<T>?;}publicstructMyStruct<T>{publicTval;}

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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

huoyaoyuan commented Jun 8, 2023

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
Member

CI failures look related?

@EgorBo

EgorBo commented Jun 8, 2023

Copy link
Copy Markdown
Member

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

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

assert(lclDsc->lvSingleDef == 0);
lclDsc->lvSingleDef = 1;
JITDUMP("Marked V%02u as a single def temp\n", tmp);
lvaSetClass(tmp, pResolvedToken->hClass);

Copy link
Copy Markdown
MemberAuthor

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?

Comment threadsrc/coreclr/jit/importer.cpp Outdated
{
assert(op1->TypeGet() == TYP_REF);

// Import isinst Nullable<V> as isinst V

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/coreclr/vm/jitinterface.cpp Outdated
break;

case CORINFO_TOKENKIND_Casting:
// Disallow ELEMENT_TYPE_BYREF and ELEMENT_TYPE_VOID

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
Member

Do we have IL coverage in jit test?

yes, look for *.ilproj in the tests folder

@huoyaoyuan

Copy link
Copy Markdown
MemberAuthor

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?

Comment threadsrc/coreclr/vm/jitinterface.cpp Outdated
COMPlusThrow(kInvalidProgramException);
break;
default:
// No additional checks. Satisfy switch exhaustiveness check.

Copy link
Copy Markdown
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
Copy Markdown
Member

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

JIT\opt\Casts may be a good place.

@jkotas

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

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
Copy Markdown
Member

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jkotas

Copy link
Copy Markdown
Member

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jkotas

Copy link
Copy Markdown
Member

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@huoyaoyuan

Copy link
Copy Markdown
MemberAuthor

Test failure should be the known one.

Comment threadsrc/coreclr/jit/importer.cpp Outdated
// 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
Copy Markdown
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.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
Contributor

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
Copy Markdown
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

EgorBo commented Jun 19, 2023

Copy link
Copy Markdown
Member

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

I've restarted the failing CI job

@huoyaoyuan

Copy link
Copy Markdown
MemberAuthor

@jkotas anything remaining?

@jkotas
jkotas merged commit 74196a0 into dotnet:mainJun 21, 2023
@jkotas

Copy link
Copy Markdown
Member

Thank you!

@huoyaoyuan
huoyaoyuan deleted the isinst-nullable branch June 21, 2023 04:40
@ghostghost locked as resolved and limited conversation to collaborators Jul 21, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIcommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@huoyaoyuan@EgorBo@jkotas@markples