Repository navigation
[Breaking] Avoid boxing on equality and comparison #1280
Description
I propose we solve dotnet/fsharp#526 (accepting the breaking change at dotnet/fsharp#9404 due to use of IEquatable<T> and IComparable<T>). We will include a BackCompat module to enable past behaviour. These current behaviours typically have no known instance of usage and the breaking change is only theoretical for code that already is designed poorly in the first place, like .Equals behaving differently from IEquatable<T> and relying on nan <> nan without using System.Double.IsNaN (IEquatable<double> makes nan = nan).
The existing way of approaching this problem in F# is taking the cost of boxing every equality and comparison for structs. Retaining the same behaviour has made F# perform worse than C#. We are keeping a reason for C# people not to move over simply because we retain a behaviour no one wants.
Anyway, it has lost me as an fsharp developer, which is a shame as I really love writing code in fsharp, but with advancements in csharp over the years, it's, for the type of code I write, a lesser worse option
Pros and Cons
The advantages of making this adjustment to F# are
- Great performance
- One less reason for C# people not to move over
- One less reason for people to abandon F#
The disadvantage of making this adjustment to F# is this is a breaking change after all. But as mentioned, it's a hypothetical breaking change only. There is no known instance of people depending on IEquatable<T> behaving differently from .Equals as this is poor design.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): S
Related suggestions:
We should do this together with another wart in FSharp.Core that has gone unnoticed, in the same update: #472
Affidavit (please submit!)
Please tick this by placing a cross in the box:
- This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
- I have searched both open and closed suggestions on this site and believe this is not a duplicate
- This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.
This is arguing against a previous decision not to accept this breaking change.
Please tick all that apply:
- This is not a breaking change to the F# language design
- I or my company would be willing to help implement and/or test this
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
Activity
T-Gro commentedon Jul 3, 2023
I personally believe the likelihood would increase if this would be an opt-in behavior, e.g. a separate project-wide setting. Like a language-feature flag which is preview only, not becoming the default for vNext automatically.
vzarytovskii commentedon Jul 3, 2023
I think for this particular change an opt out is good. Nobody will be able to explore and opt into it.
charlesroddie commentedon Jul 3, 2023
I would love to get the perf benefits here, would love to work with generic
IEquatable<'T>/IComparable<'T>rather than the non-generic ones, and would love to have the property always satisfied thatx = x(i.e. putting compliance with logic above compliance with IEEE).cartermp commentedon Aug 5, 2023
Just here to say that this:
Is something I'd put in L or XL size. There were several attempts to land this in the compiler before and one the reasons why is because it's complex and difficult to implement. There's also a long tail of legacy code that will now fail at runtime in subtle ways, which needs to be dealt with.
charlesroddie commentedon Aug 6, 2023
I would say this suggestion (deferring to
IEquatable<T>whenever possible) is S. Other approaches that were tried were L or XL because they tried to tiptoe around thenanbug.We would need a notice similar to https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/7.0/equals-nan .
It would be good to see a concrete example. Is there some existing plausible code which would depend, for correctness, on
x <> xwherex = nan?I think it's much more likely that there will be a long tail of subtle bugs that would be fixed by this change, rather than created by this change, since it is much more likely that programs would assume that
x = xrather thanx <> x.Happypig375 commentedon Aug 6, 2023
Draft RFC here fsharp/fslang-design#747
vzarytovskii commentedon Aug 6, 2023
Pretty much all code which uses floating point comparison, may implicitly rely on it. So it will be hugely runtime breaking. Another question, whether these applications relying on NaNs.
vzarytovskii commentedon Nov 6, 2023
General discussion for breaking changes in corelib: dotnet/fsharp#16231
u3r commentedon Jun 10, 2024
Question: is dotnet/fsharp#526
a) a partial fix for this
b) a full fix for this
c) unrelated?
vzarytovskii commentedon Jun 10, 2024
It's a partial fix for the problem, a workaround really, at the cost of additional codegen.
T-Gro commentedon Oct 20, 2025
I believe this suggestion needs an update to better frame the current limitations - the amount of boxing for equality in generic contexts has been reduced since this suggestion was created.
( I do not want to close the suggestion and ask for a new one because this issue has collected a lot of upvotes)
i.e. taking the dotnet/fsharp#16857 PR in mind and stating/linking remaining scenarios where it has not lead to satisfaction. (dotnet/fsharp#526 (comment) comes to mind). Some of those could be addressed as bugfixes if they do not carry a breaking change associated to them.
The remaining issues should then be contrasted with the "cost" of the break - patterns of F# code that will or will not behave differently, incl. indirect effect on collection combinators that rely on equality.
TLDR: It would make the suggestion+acceptance process easier if this issue states it here directly what is being asked and what the costs are.
smoothdeveloper commentedon Oct 24, 2026
Following up on @T-Gro's request to list scenarios that still box after dotnet/fsharp#16857:
comparison on single-field struct records (
min,max,<,compare,Array.max,sort, …) still boxes both operands on .NET 10, while equality no longer does.Since struct-record comparison is fully determined by its fields, a typed
CompareTopath, like the one #16857 added forEquals, seems possible without a semantic break, at least for records without float fields.Context
I'm working on fsdsp56k, which compiles F# kernels to Motorola DSP56300 assembly (the chip in several classic synthesizers). The same F# source also runs as a bit-exact reference on .NET and, through Fable, in JS and Rust/wasm.
Fixed-point values are modelled as single-field struct records:
The generated structural comparison has exactly the semantics we want: signed order of
Raw. Idiomaticmin,max,<and clamp are therefore correct on every target. On .NET, though, they go throughGenericLessThanIntrinsic/GenericCompareand box both operands. In a DSP inner loop that is an allocation per sample.Measurements (.NET 10.0.9,
dotnet fsi --optimize+)min a bonFraca < bonFracmin a bwithIComparable<Frac>/ custom comparisonif a.Raw < b.Raw then a else bminonint(baseline)The same numbers show up in a Release build with tiered compilation and PGO; the JIT does not remove the boxes.
Current workarounds and their cost
.Rawhelpers (Frac.Min a b): allocation-free, but non-idiomatic, and FSharp.Core combinators (Array.max,sort,maxBywithout a key) still box.(# "clt" … #)inline IL: allocation-free, but warning FS0042 and not supported by Fable.when ^T : Frac = …static conditionals: error FS0817 outside FSharp.Core.IComparable<T>: still boxes, as generic comparison goes through the non-generic path.Repro: struct-comparison-boxing.fsx
Output: