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

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

Merged
merged 3 commits into from
Apr 25, 2024

Conversation

psfinaki
Copy link
Member

@psfinaki psfinaki commented Mar 11, 2024

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>]
type SomeStruct(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)]
public struct SomeStruct : IEquatable<SomeStruct>, IStructuralEquatable, IComparable<SomeStruct>, IComparable, IStructuralComparable
{
	internal int v;

	internal int u;

	public int V => v;

	public int U => u;

	public SomeStruct(int v, int u)
	{
		this.v = v;
		this.u = u;
	}

	[CompilerGenerated]
	public sealed int CompareTo(SomeStruct obj)
	{
		IComparer genericComparer = LanguagePrimitives.GenericComparer;
		int num = v;
		int num2 = obj.v;
		int num3 = ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
		if (num3 < 0)
		{
			return num3;
		}
		if (num3 > 0)
		{
			return num3;
		}
		genericComparer = LanguagePrimitives.GenericComparer;
		num = u;
		num2 = obj.u;
		return ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
	}

	[CompilerGenerated]
	public sealed int CompareTo(object obj)
	{
		return CompareTo((SomeStruct)obj);
	}

	[CompilerGenerated]
	public sealed int CompareTo(object obj, IComparer comp)
	{
		SomeStruct someStruct = (SomeStruct)obj;
		int num = v;
		int num2 = someStruct.v;
		int num3 = ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
		if (num3 < 0)
		{
			return num3;
		}
		if (num3 > 0)
		{
			return num3;
		}
		num = u;
		num2 = someStruct.u;
		return ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
	}

	[CompilerGenerated]
	public sealed int GetHashCode(IEqualityComparer comp)
	{
		int num = 0;
		num = -1640531527 + (u + ((num << 6) + (num >> 2)));
		return -1640531527 + (v + ((num << 6) + (num >> 2)));
	}

	[CompilerGenerated]
	public sealed override int GetHashCode()
	{
		return GetHashCode(LanguagePrimitives.GenericEqualityComparer);
	}

	[CompilerGenerated]
	public sealed bool Equals(object obj, IEqualityComparer comp)
	{
		if (obj is SomeStruct someStruct)
		{
			if (v == someStruct.v)
			{
				return u == someStruct.u;
			}
			return false;
		}
		return false;
	}

	[CompilerGenerated]
	public sealed bool Equals(SomeStruct obj)
	{
		if (v == obj.v)
		{
			return u == obj.u;
		}
		return false;
	}

	[CompilerGenerated]
	public sealed override bool Equals(object obj)
	{
		if (obj is SomeStruct)
		{
			return Equals((SomeStruct)obj);
		}
		return false;
	}
}

The comparison itself is optimized to this:

public static void main@()
{
    x@1 = new Test.SomeStruct(1, 2);
    y@1 = new Test.SomeStruct(2, 3);
    arg@1 = x@1.Equals(Test.y@1, LanguagePrimitives.GenericEqualityComparer);
}

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

public sealed bool Equals(object obj, IEqualityComparer comp)
public sealed bool Equals(SomeStruct obj)
public sealed override bool Equals(object obj)

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:

.method assembly specialname static void staticInitialization@() cil managed
{
    .maxstack  8
    IL_0000:  ldc.i4.1
    IL_0001:  ldc.i4.2
    IL_0002:  newobj     instance void assembly/SomeStruct::.ctor(int32,
                                                                  int32)
    IL_0007:  stsfld     valuetype assembly/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:  stsfld     valuetype assembly/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:  box        assembly/SomeStruct
    IL_0027:  call       class [runtime]System.Collections.IEqualityComparer [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()
    IL_002c:  call       instance bool assembly/SomeStruct::Equals(object,
                                                                   class [runtime]System.Collections.IEqualityComparer)
    IL_0031:  stsfld     bool assembly::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:

public sealed bool Equals(SomeStruct obj, IEqualityComparer comp)
public sealed bool Equals(object obj, IEqualityComparer comp)
public sealed bool Equals(SomeStruct obj)
public sealed override bool Equals(object obj)

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)]
public struct SomeStruct : IEquatable<SomeStruct>, IStructuralEquatable, IComparable<SomeStruct>, IComparable, IStructuralComparable
{
	internal int v;

	internal int u;

	public int V => v;

	public int U => u;

	public SomeStruct(int v, int u)
	{
		this.v = v;
		this.u = u;
	}

	[CompilerGenerated]
	public sealed int CompareTo(SomeStruct obj)
	{
		IComparer genericComparer = LanguagePrimitives.GenericComparer;
		int num = v;
		int num2 = obj.v;
		int num3 = ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
		if (num3 < 0)
		{
			return num3;
		}
		if (num3 > 0)
		{
			return num3;
		}
		genericComparer = LanguagePrimitives.GenericComparer;
		num = u;
		num2 = obj.u;
		return ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
	}

	[CompilerGenerated]
	public sealed int CompareTo(object obj)
	{
		return CompareTo((SomeStruct)obj);
	}

	[CompilerGenerated]
	public sealed int CompareTo(object obj, IComparer comp)
	{
		SomeStruct someStruct = (SomeStruct)obj;
		int num = v;
		int num2 = someStruct.v;
		int num3 = ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
		if (num3 < 0)
		{
			return num3;
		}
		if (num3 > 0)
		{
			return num3;
		}
		num = u;
		num2 = someStruct.u;
		return ((num > num2) ? 1 : 0) - ((num < num2) ? 1 : 0);
	}

	[CompilerGenerated]
	public sealed int GetHashCode(IEqualityComparer comp)
	{
		int num = 0;
		num = -1640531527 + (u + ((num << 6) + (num >> 2)));
		return -1640531527 + (v + ((num << 6) + (num >> 2)));
	}

	[CompilerGenerated]
	public sealed override int GetHashCode()
	{
		return GetHashCode(LanguagePrimitives.GenericEqualityComparer);
	}

	[CompilerGenerated]
	public bool Equals(SomeStruct obj, IEqualityComparer comp)
	{
		if (v == obj.v)
		{
			return u == obj.u;
		}
		return false;
	}

	[CompilerGenerated]
	public sealed bool Equals(object obj, IEqualityComparer comp)
	{
		if (obj is SomeStruct obj2)
		{
			return Equals(obj2, comp);
		}
		return false;
	}

	[CompilerGenerated]
	public sealed bool Equals(SomeStruct obj)
	{
		if (v == obj.v)
		{
			return u == obj.u;
		}
		return false;
	}

	[CompilerGenerated]
	public sealed override bool Equals(object obj)
	{
		if (obj is SomeStruct)
		{
			return Equals((SomeStruct)obj);
		}
		return false;
	}
}

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

public static void main@()
{
    x@1 = new Test.SomeStruct(1, 2);
    y@1 = new Test.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:

.method assembly specialname static void staticInitialization@() cil managed
{
    .maxstack  8
    IL_0000:  ldc.i4.1
    IL_0001:  ldc.i4.2
    IL_0002:  newobj     instance void assembly/SomeStruct::.ctor(int32,
                                                                  int32)
    IL_0007:  stsfld     valuetype assembly/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:  stsfld     valuetype assembly/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:  call       class [runtime]System.Collections.IEqualityComparer [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()
    IL_0027:  call       instance bool assembly/SomeStruct::Equals(valuetype assembly/SomeStruct,
                                                                   class [runtime]System.Collections.IEqualityComparer)
    IL_002c:  stsfld     bool assembly::arg@1
    IL_0031:  ret
}

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

Method Mean Error StdDev Gen0 Allocated
Then 7.378 ns 0.2268 ns 0.6508 ns 0.0004 24 B
Now 1.8307 ns 0.0999 ns 0.2898 ns - -

Where else does it help?

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

Before:

Method Mean Error StdDev Gen0 Allocated
Struct 7.378 ns 0.2268 ns 0.6508 ns 0.0004 24 B
StructUnion 6.022 ns 0.2315 ns 0.6825 ns 0.0004 24 B
StructRecord 3.793 ns 0.0942 ns 0.1351 ns 0.0004 24 B

After:

Method Mean Error StdDev Allocated
Struct 1.8307 ns 0.0999 ns 0.2898 ns -
StructUnion 0.3812 ns 0.0684 ns 0.1995 ns -
StructRecord 2.2719 ns 0.0427 ns 0.0881 ns -

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

Before:

Method Mean Error StdDev Median Gen0 Allocated
GenericStruct 8.106 ns 0.4082 ns 1.2036 ns 7.681 ns 0.0004 24 B
GenericStructUnion 8.886 ns 0.4630 ns 1.3433 ns 8.610 ns 0.0004 24 B
GenericStructRecord 8.106 ns 0.2010 ns 0.5570 ns 8.081 ns 0.0004 24 B

After:

Method Mean Error StdDev Median Allocated
GenericStruct 2.292 ns 0.0821 ns 0.2420 ns 2.200 ns -
GenericStructUnion 4.356 ns 0.2568 ns 0.7449 ns 4.314 ns -
GenericStructRecord 3.266 ns 0.2580 ns 0.7607 ns 3.475 ns -

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

[<Struct>]
type SomeStruct(v: int) =
    member _.V = v

SomeStruct 1 = SomeStruct 2 |> ignore

The generated comparison used to be:

public static void main@()
{
    x@1 = new Test.SomeStruct(1);
    y@1 = new Test.SomeStruct(2);
    arg@1 = x@1.Equals(Test.y@1, LanguagePrimitives.GenericEqualityComparer);
}
.method public static 
	void main@ () cil managed 
{
	// Method begins at RVA 0x2190
	// Header size: 1
	// Code size: 53 (0x35)
	.maxstack 8
	.entrypoint

	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 void Test/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: box Test/SomeStruct
	IL_0025: call class [mscorlib]System.Collections.IEqualityComparer [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives::get_GenericEqualityComparer()
	IL_002a: call instance bool Test/SomeStruct::Equals(object, class [mscorlib]System.Collections.IEqualityComparer)
	IL_002f: stsfld bool '<StartupCode$test>.$Test'::arg@1
	IL_0034: ret
}

Now it is simplified to this:

public static void main@()
{
    x@1 = new Test.SomeStruct(1);
    y@1 = new Test.SomeStruct(2);
    Test.SomeStruct someStruct = Test.y@1;
    arg@1 = x@1.v == someStruct.v;
}
.method public static 
	void main@ () cil managed 
{
	// 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 void Test/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.0
	IL_001c: ldsflda valuetype Test/SomeStruct '<StartupCode$test>.$Test'::x@1
	IL_0021: ldfld int32 Test/SomeStruct::v
	IL_0026: ldloca.s 0
	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:

Method Mean Error StdDev Median Gen0 Allocated
TinyStruct 7.309 ns 0.2672 ns 0.7711 ns 7.076 ns 0.0004 24 B
TinyStructUnion 4.180 ns 0.2651 ns 0.7435 ns 3.969 ns 0.0004 24 B
TinyStructRecord 4.947 ns 0.1835 ns 0.5236 ns 4.851 ns 0.0004 24 B

After:

Method Mean Error StdDev Median Allocated
TinyStruct 0.0460 ns 0.0405 ns 0.1194 ns 0.0000 ns -
TinyStructUnion 0.0339 ns 0.0275 ns 0.0793 ns 0.0000 ns -
TinyStructRecord 0.1324 ns 0.0395 ns 0.1126 ns 0.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:

Method Mean Error StdDev Median Gen0 Allocated
ArrayContainsExisting 15.48 ns 0.398 ns 1.134 ns 15.24 ns 0.0008 48 B
ArrayContainsNonexisting 5,190.95 ns 103.533 ns 263.526 ns 5,169.08 ns 0.3891 24000 B
ArrayExistsExisting 17.97 ns 0.389 ns 1.046 ns 17.89 ns 0.0012 72 B
ArrayExistsNonexisting 5,316.64 ns 103.776 ns 148.832 ns 5,339.36 ns 0.3891 24024 B
ArrayTryFindExisting 24.80 ns 0.554 ns 1.144 ns 24.64 ns 0.0015 96 B
ArrayTryFindNonexisting 5,139.58 ns 260.949 ns 761.201 ns 4,826.52 ns 0.3891 24024 B
ArrayTryFindIndexExisting 15.92 ns 0.526 ns 1.510 ns 15.39 ns 0.0015 96 B
ArrayTryFindIndexNonexisting 4,349.13 ns 100.750 ns 282.514 ns 4,257.63 ns 0.3891 24024 B

After:

Method Mean Error StdDev Median Gen0 Allocated
ArrayContainsExisting 4.865 ns 0.3452 ns 1.0071 ns 4.359 ns - -
ArrayContainsNonexisting 766.005 ns 15.2003 ns 16.2642 ns 765.816 ns - -
ArrayExistsExisting 8.025 ns 0.1966 ns 0.3644 ns 8.061 ns 0.0004 24 B
ArrayExistsNonexisting 834.811 ns 16.2784 ns 26.7459 ns 826.627 ns - 24 B
ArrayTryFindExisting 16.401 ns 0.3932 ns 0.9864 ns 16.393 ns 0.0008 48 B
ArrayTryFindNonexisting 1,140.515 ns 22.7372 ns 50.3840 ns 1,132.769 ns - 24 B
ArrayTryFindIndexExisting 14.864 ns 0.3648 ns 0.4614 ns 14.903 ns 0.0008 48 B
ArrayTryFindIndexNonexisting 990.028 ns 19.7157 ns 49.0991 ns 988.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:

Method Mean Error StdDev Gen0 Allocated
ValueOption_Some 73.313 ns 3.0099 ns 8.7801 ns 0.0020 128 B
ValueOption_None 9.721 ns 0.2369 ns 0.6759 ns 0.0005 32 B
Result_Ok 107.970 ns 12.3481 ns 36.4086 ns 0.0021 136 B
Result_Error 79.213 ns 2.0888 ns 6.0266 ns 0.0021 136 B

After:

Method Mean Error StdDev Gen0 Allocated
ValueOption_Some 64.893 ns 3.5526 ns 10.3630 ns 0.0015 96 B
ValueOption_None 4.239 ns 0.0850 ns 0.2467 ns - -
Result_Ok 85.351 ns 9.8472 ns 29.0348 ns 0.0015 96 B
Result_Error 53.193 ns 1.0748 ns 1.5068 ns 0.0015 96 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

Copy link
Contributor

github-actions bot commented Mar 11, 2024

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/8.0.400.md

@psfinaki psfinaki changed the title WIP, DON'T REVIEW: further equality optimizations WIP: further equality optimizations Mar 15, 2024
@psfinaki psfinaki changed the title WIP: further equality optimizations [WIP] Generate new Equals overload to avoid boxing for structural comparison Mar 15, 2024
@psfinaki
Copy link
Member Author

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@psfinaki
Copy link
Member Author

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@psfinaki psfinaki force-pushed the equality-7 branch 3 times, most recently from b0a93dd to 918870a Compare March 28, 2024 10:48
@psfinaki
Copy link
Member Author

/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 owner March 28, 2024 19:24
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@psfinaki psfinaki changed the title [WIP] Generate new Equals overload to avoid boxing for structural comparison Generate new Equals overload to avoid boxing for structural comparison Mar 28, 2024
@psfinaki psfinaki requested a review from dsyme March 28, 2024 19:25
@T-Gro
Copy link
Member

T-Gro commented Apr 2, 2024

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

@psfinaki
Copy link
Member Author

psfinaki commented Apr 2, 2024

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.

@psfinaki psfinaki force-pushed the equality-7 branch 2 times, most recently from f45f8ef to 22c58bc Compare April 16, 2024 16:40
@auduchinok
Copy link
Member

This also applies to struct unions in the same fashion:

Should it also cover struct records?

@psfinaki
Copy link
Member Author

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 :)

Copy link
Contributor

@dsyme dsyme left a comment

Choose a reason for hiding this comment

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

Fantastic work!

Copy link
Member

@KevinRansom KevinRansom left a comment

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:main Apr 25, 2024
32 checks passed
@psfinaki psfinaki deleted the equality-7 branch April 25, 2024 19:06
@auduchinok
Copy link
Member

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

@psfinaki
Copy link
Member Author

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
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
Member Author

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

auduchinok commented May 15, 2024

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

abelbraaksma commented May 16, 2024

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
Member Author

@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
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 free to 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