Skip to content

Improve EqualityComparer for NativeAOT - #83054

Merged
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
EgorBo:naot-equality-comparer-fix
Mar 7, 2023
Merged

Improve EqualityComparer for NativeAOT#83054
MichalStrehovsky merged 2 commits into
dotnet:mainfrom
EgorBo:naot-equality-comparer-fix

Conversation

@EgorBo

Copy link
Copy Markdown
Member

Closes#81592

staticboolTest(inta,intb)=>EqualityComparer<int>.Default.Equals(a,b);

Old codegen on NativeAOT:

; Method Program:Bar[int](int,int):bool57pushrdi56pushrsi 4883EC28 subrsp,40 8BF1 movesi,ecx 8BFA movedi,edx 488B0500000000 movrax, qword ptr [(reloc 0x4000000000425988)]4883780800cmp gword ptr [rax+08H],07505jne SHORT G_M49161_IG04 E800000000 call System.Collections.Generic.EqualityComparer`1[int]:Create()G_M49161_IG04:  33C0 xoreax,eax 3BF7 cmpesi,edi 0F94C0 sete al 4883C428 addrsp,40 5E poprsi 5F poprdi C3 ret; Total bytes of code: 43

New codegen:

; Method Program:Bar[int](int,int):bool 33C0 xoreax,eax 3BCA cmpecx,edx 0F94C0 sete al C3 ret; Total bytes of code: 8

cc @dotnet/ilc-contrib @MichalStrehovsky

@ghost

ghost commented Mar 7, 2023

Copy link
Copy Markdown

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

Issue Details

Closes #81592

staticboolTest(inta,intb)=>EqualityComparer<int>.Default.Equals(a,b);

Old codegen on NativeAOT:

; Method Program:Bar[int](int,int):bool57pushrdi56pushrsi 4883EC28 subrsp,40 8BF1 movesi,ecx 8BFA movedi,edx 488B0500000000 movrax, qword ptr [(reloc 0x4000000000425988)]4883780800cmp gword ptr [rax+08H],07505jne SHORT G_M49161_IG04 E800000000 call System.Collections.Generic.EqualityComparer`1[int]:Create()G_M49161_IG04:  33C0 xoreax,eax 3BF7 cmpesi,edi 0F94C0 sete al 4883C428 addrsp,40 5E poprsi 5F poprdi C3 ret; Total bytes of code: 43

New codegen:

; Method Program:Bar[int](int,int):bool 33C0 xoreax,eax 3BCA cmpecx,edx 0F94C0 sete al C3 ret; Total bytes of code: 8

cc @dotnet/ilc-contrib @MichalStrehovsky

Author:EgorBo
Assignees:-
Labels:

area-NativeAOT-coreclr

Milestone:-

@EgorBo

Copy link
Copy Markdown
MemberAuthor

More complicated example involving generics:

usingSystem.Collections.Generic;usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Foo(1,2);Foo("1","2");}[MethodImpl(MethodImplOptions.NoInlining)]staticboolFoo<T>(Ta,Tb)=>Bar(a,b);[MethodImpl(MethodImplOptions.NoInlining)]staticboolBar<T>(Ta,Tb)=>EqualityComparer<T>.Default.Equals(a,b);}

codegen diff: https://www.diffchecker.com/MyttN9cW/

@MichalStrehovskyMichalStrehovsky 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.

Nice!

@MichalStrehovsky
MichalStrehovsky merged commit 1247d8f into dotnet:mainMar 7, 2023
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Mar 7, 2023
If a type is preinitialized, we shouldn't need the cctor pointer anymore. This was the only thing keeping around general-purpose comparers logic after @EgorBo's dotnet#83054.
MichalStrehovsky added a commit that referenced this pull request Mar 7, 2023
If a type is preinitialized, we shouldn't need the cctor pointer anymore. This was the only thing keeping around general-purpose comparers logic after @EgorBo's #83054.
@EgorBo
EgorBo deleted the naot-equality-comparer-fix branch March 7, 2023 08:37
@eerhardt

Copy link
Copy Markdown
Member

It looks like this caused a ~50KB size on disk regression on the BasicMinimalApi app on linux-x64.

The regression happened in this commit range.

Doing an analysis of the .mstat files before and after, I see most of the size increase in System.Collections.Generic.

image

And then dumping the difference in that namespace, the biggest chunk that stands out:

image

(note there are many more ObjectComparers on the right, these are just a snippet of them. There are also new ObjectEqualityComparer and GenericEqualityComparer instances as well.)

Attached are the mstat files and dumping them to txt.
BasicMinimalApi-regression.zip

@agocke

Copy link
Copy Markdown
Member

Is it possible that #83063 will bring things back into alignment?

@eerhardt

Copy link
Copy Markdown
Member

Is it possible that #83063 will bring things back into alignment?

Unfortunately, no. Note that both this commit and that commit are in the above commit range. So the 50KB regression is after both changes.

@eerhardt

Copy link
Copy Markdown
Member

Also note that the regression on win-x64 seems to be smaller. More like ~20KB.

@EgorBo

Copy link
Copy Markdown
MemberAuthor

@MichalStrehovsky any ideas what we can do here to avoid type loading in runtime? if not - can we somehow do lazy init only for non-value type instanciations?

@MichalStrehovsky

Copy link
Copy Markdown
Member

I looked at this in a bit more detail. Previously, comparer types for reference type T were always loaded lazily - we generated the code for "any reference type", and at runtime we would land here where we essentially MakeGenericType the right comparer:

internalstaticobjectGetComparer(RuntimeTypeHandlet)
{
RuntimeTypeHandlecomparerType;
RuntimeTypeHandleopenComparerType=default(RuntimeTypeHandle);
RuntimeTypeHandlecomparerTypeArgument=default(RuntimeTypeHandle);
if(RuntimeAugments.IsNullable(t))
{
RuntimeTypeHandlenullableType=RuntimeAugments.GetNullableType(t);
openComparerType=typeof(NullableComparer<>).TypeHandle;
comparerTypeArgument=nullableType;
}
if(EqualityComparerHelpers.IsEnum(t))
{
openComparerType=typeof(EnumComparer<>).TypeHandle;
comparerTypeArgument=t;
}
if(openComparerType.Equals(default(RuntimeTypeHandle)))
{
if(ImplementsIComparable(t))
{
openComparerType=typeof(GenericComparer<>).TypeHandle;
comparerTypeArgument=t;
}
else
{
openComparerType=typeof(ObjectComparer<>).TypeHandle;
comparerTypeArgument=t;
}
}
boolsuccess=RuntimeAugments.TypeLoaderCallbacks.TryGetConstructedGenericTypeForComponents(openComparerType,newRuntimeTypeHandle[]{comparerTypeArgument},outcomparerType);
if(!success)
{
Environment.FailFast("Unable to create comparer");
}
returnRuntimeAugments.NewObject(comparerType);
}

With the change in this PR, comparers are preinitialized at compile time (we actually preallocate the object instance).

It is a compromise. Doing things lazily helps size, but impacts startup. For the BasicWebApi sample, we were previously loading 3 comparers at startup and another 4 on first request. So we avoid 7 type loads. The 7 type loads cost about 0.2 ms in startup time in total. So we're basically looking at <0.5% size regression for <0.5% startup improvement.

If it was more than 0.5% I would be more concerned but maybe this is an acceptable tradeoff?

@eerhardt

Copy link
Copy Markdown
Member

but maybe this is an acceptable tradeoff?

Yeah, it sounds like it. I just wanted to raise the size regression here just to see if there was anything we could do. But since this is so small, if there isn't anything trivial/obvious we can just live with it.

@MichalPetryka

Copy link
Copy Markdown
Contributor

Can this be removed now?

#if !NATIVEAOT
returnEqualityComparer<T>.Default.IndexOf(array,value,startIndex,count);
#else
returnIndexOfImpl(array,value,startIndex,count);
#endif

@MichalStrehovsky

Copy link
Copy Markdown
Member

Can this be removed now?

It would likely bring a lot more comparers for no clear benefit.

@ghostghost locked as resolved and limited conversation to collaborators Apr 7, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NativeAOT] Usages of generic EqualityComparer.Default have cctor init checks for primitives

5 participants

@EgorBo@eerhardt@agocke@MichalStrehovsky@MichalPetryka