Skip to content

Generate new Equals overload to avoid boxing for structural comparison - #16857

Merged
psfinaki merged 3 commits into
dotnet:mainfrom
psfinaki:equality-7
Apr 25, 2024
Merged

Generate new Equals overload to avoid boxing for structural comparison#16857
psfinaki merged 3 commits into
dotnet:mainfrom
psfinaki:equality-7

Conversation

@psfinaki

@psfinakipsfinaki commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

TL;DR

Stop boxing when doing equality test on structs.


And I believe this closes#526. The discussion kind of diverged there but all in all this eliminates the initial problem mentioned, which is also by far the most common usecase of those discussed in the ticket.

What's the problem?

Let's define a struct and compare its two instances:

[<Struct>]typeSomeStruct(v: int,u: int)=member_.V= v
member_.U= u
SomeStruct(1,2)= SomeStruct(2,3)|> ignore

What's going on here?

For SomeStruct, we generate a ctor and implementations for a few handy interfaces:

Generated augmentations
[Serializable][Struct][CompilationMapping(SourceConstructFlags.ObjectType)]publicstructSomeStruct:IEquatable<SomeStruct>,IStructuralEquatable,IComparable<SomeStruct>,IComparable,IStructuralComparable{internalintv;internalintu;publicintV=>v;publicintU=>u;publicSomeStruct(intv,intu){this.v=v;this.u=u;}[CompilerGenerated]publicsealedintCompareTo(SomeStructobj){IComparergenericComparer=LanguagePrimitives.GenericComparer;intnum=v;intnum2=obj.v;intnum3=((num>num2)?1:0)-((num<num2)?1:0);if(num3<0){returnnum3;}if(num3>0){returnnum3;}genericComparer=LanguagePrimitives.GenericComparer;num=u;num2=obj.u;return((num>num2)?1:0)-((num<num2)?1:0);}[CompilerGenerated]publicsealedintCompareTo(objectobj){returnCompareTo((SomeStruct)obj);}[CompilerGenerated]publicsealedintCompareTo(objectobj,IComparercomp){SomeStructsomeStruct=(SomeStruct)obj;intnum=v;intnum2=someStruct.v;intnum3=((num>num2)?1:0)-((num<num2)?1:0);if(num3<0){returnnum3;}if(num3>0){returnnum3;}num=u;num2=someStruct.u;return((num>num2)?1:0)-((num<num2)?1:0);}[CompilerGenerated]publicsealedintGetHashCode(IEqualityComparercomp){intnum=0;num=-1640531527+(u+((num<<6)+(num>>2)));return-1640531527+(v+((num<<6)+(num>>2)));}[CompilerGenerated]publicsealedoverrideintGetHashCode(){returnGetHashCode(LanguagePrimitives.GenericEqualityComparer);}[CompilerGenerated]publicsealedboolEquals(objectobj,IEqualityComparercomp){if(objisSomeStructsomeStruct){if(v==someStruct.v){returnu==someStruct.u;}returnfalse;}returnfalse;}[CompilerGenerated]publicsealedboolEquals(SomeStructobj){if(v==obj.v){returnu==obj.u;}returnfalse;}[CompilerGenerated]publicsealedoverrideboolEquals(objectobj){if(objisSomeStruct){returnEquals((SomeStruct)obj);}returnfalse;}}

The comparison itself is optimized to this:

publicstaticvoidmain@(){
x@1=newTest.SomeStruct(1,2);
y@1=newTest.SomeStruct(2,3);
arg@1= x@1.Equals(Test.y@1,LanguagePrimitives.GenericEqualityComparer);}

Which Equals of the generated ones is used? We have three:

publicsealedboolEquals(objectobj,IEqualityComparercomp)publicsealedboolEquals(SomeStructobj)publicsealedoverrideboolEquals(objectobj)

Despite the fact that we know the "exact" type of compared things (as in the second overload), we have to call the first one due to the fact it's the only one with two parameters. And since it's first argument is object, there is boxing happening:

.methodassembly specialname staticvoidstaticInitialization@()cilmanaged{.maxstack 8IL_0000:ldc.i4.1IL_0001: ldc.i4.2IL_0002: newobj instance voidassembly/SomeStruct::.ctor(int32,int32)IL_0007:stsfldvaluetypeassembly/SomeStruct assembly::x@1
IL_000c: ldc.i4.2
IL_000d: ldc.i4.3
IL_000e: newobj instance void assembly/SomeStruct::.ctor(int32,int32)IL_0013:stsfldvaluetypeassembly/SomeStruct assembly::y@1
IL_0018: ldsflda valuetype assembly/SomeStruct assembly::x@1
IL_001d: call valuetype assembly/SomeStruct assembly::get_y@1()IL_0022:boxassembly/SomeStruct
IL_0027: call class[runtime]System.Collections.IEqualityComparer[FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()
IL_002c: call instance boolassembly/SomeStruct::Equals(object,class[runtime]System.Collections.IEqualityComparer)IL_0031: stsfld boolassembly::arg@1
IL_0036: ret
}

The problematic operation is at IL_0022. This is not good - doing redundant operations takes unnecessary time and memory.

What's the solution?

We now generate a new Equals overload to be called in these cases. It has the "exact" type instead of the object type as an argument:

publicsealedboolEquals(SomeStructobj,IEqualityComparercomp)publicsealedboolEquals(objectobj,IEqualityComparercomp)publicsealedboolEquals(SomeStructobj)publicsealedoverrideboolEquals(objectobj)

Besides, the other two-parameter overload now calls this one, similar to how it is happening with the one-parameter overloads. Here is how things look like:

Generated augmentations
[Serializable][Struct][CompilationMapping(SourceConstructFlags.ObjectType)]publicstructSomeStruct:IEquatable<SomeStruct>,IStructuralEquatable,IComparable<SomeStruct>,IComparable,IStructuralComparable{internalintv;internalintu;publicintV=>v;publicintU=>u;publicSomeStruct(intv,intu){this.v=v;this.u=u;}[CompilerGenerated]publicsealedintCompareTo(SomeStructobj){IComparergenericComparer=LanguagePrimitives.GenericComparer;intnum=v;intnum2=obj.v;intnum3=((num>num2)?1:0)-((num<num2)?1:0);if(num3<0){returnnum3;}if(num3>0){returnnum3;}genericComparer=LanguagePrimitives.GenericComparer;num=u;num2=obj.u;return((num>num2)?1:0)-((num<num2)?1:0);}[CompilerGenerated]publicsealedintCompareTo(objectobj){returnCompareTo((SomeStruct)obj);}[CompilerGenerated]publicsealedintCompareTo(objectobj,IComparercomp){SomeStructsomeStruct=(SomeStruct)obj;intnum=v;intnum2=someStruct.v;intnum3=((num>num2)?1:0)-((num<num2)?1:0);if(num3<0){returnnum3;}if(num3>0){returnnum3;}num=u;num2=someStruct.u;return((num>num2)?1:0)-((num<num2)?1:0);}[CompilerGenerated]publicsealedintGetHashCode(IEqualityComparercomp){intnum=0;num=-1640531527+(u+((num<<6)+(num>>2)));return-1640531527+(v+((num<<6)+(num>>2)));}[CompilerGenerated]publicsealedoverrideintGetHashCode(){returnGetHashCode(LanguagePrimitives.GenericEqualityComparer);}[CompilerGenerated]publicboolEquals(SomeStructobj,IEqualityComparercomp){if(v==obj.v){returnu==obj.u;}returnfalse;}[CompilerGenerated]publicsealedboolEquals(objectobj,IEqualityComparercomp){if(objisSomeStructobj2){returnEquals(obj2,comp);}returnfalse;}[CompilerGenerated]publicsealedboolEquals(SomeStructobj){if(v==obj.v){returnu==obj.u;}returnfalse;}[CompilerGenerated]publicsealedoverrideboolEquals(objectobj){if(objisSomeStruct){returnEquals((SomeStruct)obj);}returnfalse;}}

Now, for the equality test, the generate code looks exactly the same:

publicstaticvoidmain@(){
x@1=newTest.SomeStruct(1,2);
y@1=newTest.SomeStruct(2,3);
arg@1= x@1.Equals(Test.y@1,LanguagePrimitives.GenericEqualityComparer);}

However, it's a different overload that is called now, hence the IL differs:

.methodassembly specialname staticvoidstaticInitialization@()cilmanaged{.maxstack 8IL_0000:ldc.i4.1IL_0001: ldc.i4.2IL_0002: newobj instance voidassembly/SomeStruct::.ctor(int32,int32)IL_0007:stsfldvaluetypeassembly/SomeStruct assembly::x@1
IL_000c: ldc.i4.2
IL_000d: ldc.i4.3
IL_000e: newobj instance void assembly/SomeStruct::.ctor(int32,int32)IL_0013:stsfldvaluetypeassembly/SomeStruct assembly::y@1
IL_0018: ldsflda valuetype assembly/SomeStruct assembly::x@1
IL_001d: call valuetype assembly/SomeStruct assembly::get_y@1()IL_0022:callclass[runtime]System.Collections.IEqualityComparer[FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()
IL_0027: call instance boolassembly/SomeStruct::Equals(valuetypeassembly/SomeStruct,class[runtime]System.Collections.IEqualityComparer)IL_002c: stsfld boolassembly::arg@1
IL_0031: ret
}

There is no more boxing involved in here. This is confirmed by benchmarks:

MethodMeanErrorStdDevGen0Allocated
Then7.378 ns0.2268 ns0.6508 ns0.000424 B
Now1.8307 ns0.0999 ns0.2898 ns--

Where else does it help?

This also applies to struct unions and struct records in the same fashion.

Before:

MethodMeanErrorStdDevGen0Allocated
Struct7.378 ns0.2268 ns0.6508 ns0.000424 B
StructUnion6.022 ns0.2315 ns0.6825 ns0.000424 B
StructRecord3.793 ns0.0942 ns0.1351 ns0.000424 B

After:

MethodMeanErrorStdDevAllocated
Struct1.8307 ns0.0999 ns0.2898 ns-
StructUnion0.3812 ns0.0684 ns0.1995 ns-
StructRecord2.2719 ns0.0427 ns0.0881 ns-

This also applies to the generic versions of the above.¨

Before:

MethodMeanErrorStdDevMedianGen0Allocated
GenericStruct8.106 ns0.4082 ns1.2036 ns7.681 ns0.000424 B
GenericStructUnion8.886 ns0.4630 ns1.3433 ns8.610 ns0.000424 B
GenericStructRecord8.106 ns0.2010 ns0.5570 ns8.081 ns0.000424 B

After:

MethodMeanErrorStdDevMedianAllocated
GenericStruct2.292 ns0.0821 ns0.2420 ns2.200 ns-
GenericStructUnion4.356 ns0.2568 ns0.7449 ns4.314 ns-
GenericStructRecord3.266 ns0.2580 ns0.7607 ns3.475 ns-

In case of one-member constructs, the comparison gets inlined. E.g. consider this code:

[<Struct>]typeSomeStruct(v: int)=member_.V= v
SomeStruct 1= SomeStruct 2|> ignore

The generated comparison used to be:

publicstaticvoidmain@(){
x@1=newTest.SomeStruct(1);
y@1=newTest.SomeStruct(2);
arg@1= x@1.Equals(Test.y@1,LanguagePrimitives.GenericEqualityComparer);}
.methodpublicstaticvoidmain@ ()cilmanaged{// Method begins at RVA 0x2190// Header size: 1// Code size: 53 (0x35).maxstack 8.entrypoint
IL_0000: ldc.i4.1
IL_0001: newobj instance voidTest/SomeStruct::.ctor(int32)
IL_0006: stsfld valuetype Test/SomeStruct '<StartupCode$test>.$Test'::x@1
IL_000b:ldc.i4.2IL_000c:newobj instance voidTest/SomeStruct::.ctor(int32)
IL_0011: stsfld valuetype Test/SomeStruct '<StartupCode$test>.$Test'::y@1
IL_0016: ldsflda valuetype Test/SomeStruct '<StartupCode$test>.$Test'::x@1
IL_001b: call valuetype Test/SomeStruct Test::get_y@1()IL_0020:boxTest/SomeStruct
IL_0025:callclass[mscorlib]System.Collections.IEqualityComparer[FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()IL_002a:callinstance bool Test/SomeStruct::Equals(object,class[mscorlib]System.Collections.IEqualityComparer)IL_002f:stsfldbool '<StartupCode$test>.$Test'::arg@1
IL_0034: ret
}

Now it is simplified to this:

publicstaticvoidmain@(){
x@1=newTest.SomeStruct(1);
y@1=newTest.SomeStruct(2);Test.SomeStructsomeStruct=Test.y@1;
arg@1= x@1.v==someStruct.v;}
.methodpublicstaticvoidmain@ ()cilmanaged{// Method begins at RVA 0x21a4// Header size: 12// Code size: 53 (0x35).maxstack 4.entrypoint
.locals init([0]valuetype Test/SomeStruct
)
IL_0000: ldc.i4.1
IL_0001: newobj instance void Test/SomeStruct::.ctor(int32)
IL_0006: stsfld valuetype Test/SomeStruct '<StartupCode$test>.$Test'::x@1
IL_000b: ldc.i4.2
IL_000c: newobj instance voidTest/SomeStruct::.ctor(int32)
IL_0011: stsfld valuetype Test/SomeStruct '<StartupCode$test>.$Test'::y@1
IL_0016: call valuetype Test/SomeStruct Test::get_y@1()
IL_001b:stloc.0IL_001c:ldsflda valuetype Test/SomeStruct '<StartupCode$test>.$Test'::x@1
IL_0021: ldfld int32 Test/SomeStruct::v
IL_0026:ldloca.s0
IL_0028: ldfld int32 Test/SomeStruct::v
IL_002d: ceq
IL_002f: stsfld bool '<StartupCode$test>.$Test'::arg@1
IL_0034: ret
}

This leads to even bigger speed reductions in all such cases.

Before:

MethodMeanErrorStdDevMedianGen0Allocated
TinyStruct7.309 ns0.2672 ns0.7711 ns7.076 ns0.000424 B
TinyStructUnion4.180 ns0.2651 ns0.7435 ns3.969 ns0.000424 B
TinyStructRecord4.947 ns0.1835 ns0.5236 ns4.851 ns0.000424 B

After:

MethodMeanErrorStdDevMedianAllocated
TinyStruct0.0460 ns0.0405 ns0.1194 ns0.0000 ns-
TinyStructUnion0.0339 ns0.0275 ns0.0793 ns0.0000 ns-
TinyStructRecord0.1324 ns0.0395 ns0.1126 ns0.0939 ns-

Practical applications

From simple equality tests to real world scenarios.

Here are some examples for array functions, where equality is involved. The bigger the array is, and the more equality tests are performed, the higher are the gains. The following are variations an "exists" functionality, the optimistic and pessimistic cases.

Before:

MethodMeanErrorStdDevMedianGen0Allocated
ArrayContainsExisting15.48 ns0.398 ns1.134 ns15.24 ns0.000848 B
ArrayContainsNonexisting5,190.95 ns103.533 ns263.526 ns5,169.08 ns0.389124000 B
ArrayExistsExisting17.97 ns0.389 ns1.046 ns17.89 ns0.001272 B
ArrayExistsNonexisting5,316.64 ns103.776 ns148.832 ns5,339.36 ns0.389124024 B
ArrayTryFindExisting24.80 ns0.554 ns1.144 ns24.64 ns0.001596 B
ArrayTryFindNonexisting5,139.58 ns260.949 ns761.201 ns4,826.52 ns0.389124024 B
ArrayTryFindIndexExisting15.92 ns0.526 ns1.510 ns15.39 ns0.001596 B
ArrayTryFindIndexNonexisting4,349.13 ns100.750 ns282.514 ns4,257.63 ns0.389124024 B

After:

MethodMeanErrorStdDevMedianGen0Allocated
ArrayContainsExisting4.865 ns0.3452 ns1.0071 ns4.359 ns--
ArrayContainsNonexisting766.005 ns15.2003 ns16.2642 ns765.816 ns--
ArrayExistsExisting8.025 ns0.1966 ns0.3644 ns8.061 ns0.000424 B
ArrayExistsNonexisting834.811 ns16.2784 ns26.7459 ns826.627 ns-24 B
ArrayTryFindExisting16.401 ns0.3932 ns0.9864 ns16.393 ns0.000848 B
ArrayTryFindNonexisting1,140.515 ns22.7372 ns50.3840 ns1,132.769 ns-24 B
ArrayTryFindIndexExisting14.864 ns0.3648 ns0.4614 ns14.903 ns0.000848 B
ArrayTryFindIndexNonexisting990.028 ns19.7157 ns49.0991 ns988.739 ns-24 B

The feature also works cross-assembly. Among other things, this means that equality tests on F# builtin struct types also become faster and less-allocating.

Before:

MethodMeanErrorStdDevGen0Allocated
ValueOption_Some73.313 ns3.0099 ns8.7801 ns0.0020128 B
ValueOption_None9.721 ns0.2369 ns0.6759 ns0.000532 B
Result_Ok107.970 ns12.3481 ns36.4086 ns0.0021136 B
Result_Error79.213 ns2.0888 ns6.0266 ns0.0021136 B

After:

MethodMeanErrorStdDevGen0Allocated
ValueOption_Some64.893 ns3.5526 ns10.3630 ns0.001596 B
ValueOption_None4.239 ns0.0850 ns0.2467 ns--
Result_Ok85.351 ns9.8472 ns29.0348 ns0.001596 B
Result_Error53.193 ns1.0748 ns1.5068 ns0.001596 B

Implementation

  • The new overload is generated in AugmentWithHashCompare.fs
  • The application of the new overload is happening in Optimizer.fs
  • Everything else in the source folder is basically propagation of the new overload
  • The component tests for the new functionality are Equals10 - Equals21
  • Other component tests baselines are updated due to the new overload
  • The benchmarks are in the ExactEquals.fs files in MicroPerf
  • Since many core APIs are affected, surface areas are also updated
  • Unfortunately, the API surface differs when the real internal signature is off, hence there is a hack to ignore this

@github-actions

github-actionsBot commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Change pathRelease notes pathDescription
src/Compilerdocs/release-notes/.FSharp.Compiler.Service/8.0.400.md

@psfinakipsfinaki changed the title WIP, DON'T REVIEW: further equality optimizationsWIP: further equality optimizationsMar 15, 2024
@psfinakipsfinaki changed the title WIP: further equality optimizations[WIP] Generate new Equals overload to avoid boxing for structural comparisonMar 15, 2024
@psfinaki

Copy link
Copy Markdown
ContributorAuthor

/azp run

@azure-pipelines

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

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

/azp run

@azure-pipelines

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

@psfinaki
psfinakiforce-pushed the equality-7 branch 3 times, most recently from b0a93dd to 918870aCompareMarch 28, 2024 10:48
@psfinaki

Copy link
Copy Markdown
ContributorAuthor

/azp run

@psfinaki
psfinaki marked this pull request as ready for review March 28, 2024 19:24
@psfinaki
psfinaki requested a review from a team as a code ownerMarch 28, 2024 19:24
@azure-pipelines

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

@psfinakipsfinaki changed the title [WIP] Generate new Equals overload to avoid boxing for structural comparisonGenerate new Equals overload to avoid boxing for structural comparisonMar 28, 2024
@psfinaki
psfinaki requested a review from dsymeMarch 28, 2024 19:25
@T-Gro

T-Gro commented Apr 2, 2024

Copy link
Copy Markdown
Member

(I am assuming this is ready for review and merge now)

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

The baselines are driving me crazy here, I wanted to open it for review once they are ready. But otherwise yes, all the actual changes I wanted to have here are in.

Comment threadsrc/Compiler/Checking/AugmentWithHashCompare.fs Outdated
Comment threadsrc/Compiler/TypedTree/TypedTreePickle.fs Outdated
Comment threadsrc/Compiler/Checking/AugmentWithHashCompare.fs Outdated
Comment threadsrc/Compiler/Checking/AugmentWithHashCompare.fs Outdated
Comment threadsrc/Compiler/Checking/AugmentWithHashCompare.fsi Outdated
Comment threadsrc/Compiler/Optimize/Optimizer.fs Outdated
Comment threadsrc/Compiler/Optimize/Optimizer.fs Outdated
@psfinaki
psfinakiforce-pushed the equality-7 branch 2 times, most recently from f45f8ef to 22c58bcCompareApril 16, 2024 16:40
@auduchinok

Copy link
Copy Markdown
Member

This also applies to struct unions in the same fashion:

Should it also cover struct records?

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

This also applies to struct unions in the same fashion:

Should it also cover struct records?

@auduchinok yep, it does. At that point, I just hadn't finished with updating the PR description. Now it's there :)

@dsymedsyme left a comment

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.

Fantastic work!

Comment threadsrc/Compiler/Optimize/Optimizer.fs Outdated
Comment threadsrc/Compiler/Checking/CheckDeclarations.fs Outdated

@KevinRansomKevinRansom left a comment

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.

Looks good

@psfinaki
psfinaki merged commit 353560e into dotnet:mainApr 25, 2024
@psfinaki
psfinaki deleted the equality-7 branch April 25, 2024 19:06
@auduchinok

Copy link
Copy Markdown
Member

@psfinaki Am I missing something, or this change isn't guarded by a language version check?

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

No, there was a discussion and eventually we decided not to put it here. I am trying to remember the motivation though.

@auduchinok

Copy link
Copy Markdown
Member

The generated members are visible to the outside code, from the analysis point of view this may be a significant change, especially in language interop scenarios, which should normally be guarded by a language version.

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

@auduchinok I believe the motivation was that it is not worth the effort (it's doable but given the amount of the affected baselines it will be quite messy).

What are the particular downsides of not having it here? We use lang versions to allow teams using mixed versions of F# on their machines. So the product code would use stable F# where the devs could use the latest version if needed. This change doesn't affect semantics much and can be seen as an impl detail - albeit a large one.

@auduchinok

auduchinok commented May 15, 2024

Copy link
Copy Markdown
Member

What are the particular downsides of not having it here?

We need to provide info about generated F# symbols to C# and other languages analysis, and this should be inline with what gets compiled. This analysis does boxing checking, among other things, so it has to know about these new members too, but only when they do actually exist. Language versions is what allows to use different rules properly on such changes to the language.

@abelbraaksma

abelbraaksma commented May 16, 2024

Copy link
Copy Markdown
Contributor

This PR seems to address this draft RFC (fsharp/fslang-design#747), even though that RFC suggested a slightly different approach (that is, it also addresses the nan <> nan = true issue).

I'm not quite sure how much overlap there is between that one and this PR, perhaps we should align them so that we can publish this amazing PR with the accompanying RFC.

@psfinaki

Copy link
Copy Markdown
ContributorAuthor

@abelbraaksma thanks for your nice words :)

So yeah this hasn't touched the existing NaN inconsistency - it is still inconsistent. The approach taken in this PR is also substantially different from previous attempts to reduce boxing, but that's because those attempts tried to remove boxing in a different place - basically to use a less generic (and less boxing) comparer. This was finally done earlier this year here - although not including all the original ideas so there is much more to be done in that space.

Anyway, the issue which is focused on the NaN problem is here, I added some summary there now.

@vzarytovskii

Copy link
Copy Markdown
Member

What are the particular downsides of not having it here?

We need to provide info about generated F# symbols to C# and other languages analysis, and this should be inline with what gets compiled. This analysis does boxing checking, among other things, so it has to know about these new members too, but only when they do actually exist. Language versions is what allows to use different rules properly on such changes to the language.

I believe I'm misunderstanding things, but won't you be able to tell if it's generated by just getting all methods of the type? How having it behind language version will change the analysis. Can you give an example what will not work or work differently?

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Equality operator causes boxing on value types

7 participants

@psfinaki@T-Gro@auduchinok@abelbraaksma@vzarytovskii@KevinRansom@dsyme