Skip to content

Commit 353560e

Browse files
Generate new Equals overload to avoid boxing for structural comparison (#16857)
* Generate new Equals overload to avoid boxing for structural comparison * More benchmarks --------- Co-authored-by: Kevin Ransom (msft) <[email protected]>
1 parent 9f14733 commit 353560e

File tree

212 files changed

+33691
-17650
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

212 files changed

+33691
-17650
lines changed

docs/optimizations-equality.md

+2-42
Original file line numberDiff line numberDiff line change
@@ -209,51 +209,11 @@ let f (x: float32) (y: float32) = (x = y)
209209

210210
### F# struct type (records, tuples - with compiler-generated structural equality)
211211

212-
* Semantics: User expects field-by-field structural equality with no boxing
212+
* Semantics: User expects field-by-field structural equality
213213
* Perf expected: no boxing
214214
* Compilation today: `GenericEqualityIntrinsic<SomeStructType>`
215-
* Perf today: always boxes (Problem3 ❌)
215+
* Perf today: good ✅
216216
* [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA)
217-
* Note: the optimization path is a bit strange here, see the reductions below
218-
219-
<details>
220-
221-
<summary>Details</summary>
222-
223-
```fsharp
224-
(x = y)
225-
226-
--inline-->
227-
228-
GenericEquality x y
229-
230-
--inline-->
231-
232-
GenericEqualityFast x y
233-
234-
--inline-->
235-
236-
GenericEqualityIntrinsic x y
237-
238-
--devirtualize-->
239-
240-
x.Equals(box y, LanguagePrimitives.GenericEqualityComparer);
241-
```
242-
243-
The struct type has these generated methods:
244-
```csharp
245-
override bool Equals(object y)
246-
override bool Equals(SomeStruct obj)
247-
override bool Equals(object obj, IEqualityComparer comp) //with EqualsVal
248-
```
249-
250-
These call each other in sequence, boxing then unboxing then boxing. We do NOT generate this method, we probably should:
251-
252-
```csharp
253-
override bool Equals(SomeStruct obj, IEqualityComparer comp) //with EqualsValUnboxed
254-
```
255-
256-
If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing.
257217

258218
</details>
259219

docs/release-notes/.FSharp.Compiler.Service/8.0.400.md

+4
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@
99
* Fix bug in optimization of for-loops over integral ranges with steps and units of measure. ([Issue #17025](https://github.com/dotnet/fsharp/issues/17025), [PR #17040](https://github.com/dotnet/fsharp/pull/17040), [PR #17048](https://github.com/dotnet/fsharp/pull/17048))
1010
* Fix calling an overridden virtual static method via the interface ([PR #17013](https://github.com/dotnet/fsharp/pull/17013))
1111
* Fix state machines compilation, when big decision trees are involved, by removing code split when resumable code is detected ([PR #17076](https://github.com/dotnet/fsharp/pull/17076))
12+
13+
### Added
14+
15+
* Generate new `Equals` overload to avoid boxing for structural comparison ([PR #16857](https://github.com/dotnet/fsharp/pull/16857))

eng/Build.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,11 @@ function Process-Arguments() {
214214

215215
if ($norealsig) {
216216
$script:realsig = $False;
217+
$env:FSHARP_REALSIG="false"
217218
}
218219
else {
219220
$script:realsig = $True;
221+
$env:FSHARP_REALSIG="true"
220222
}
221223
if ($verifypackageshipstatus) {
222224
$script:verifypackageshipstatus = $True;

src/Compiler/Checking/AugmentWithHashCompare.fs

+101-14
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ let mkEqualsTy g ty =
106106
let mkEqualsWithComparerTy g ty =
107107
mkFunTy g (mkThisTy g ty) (mkFunTy g (mkRefTupledTy g [ g.obj_ty; g.IEqualityComparer_ty ]) g.bool_ty)
108108

109+
let mkEqualsWithComparerTyExact g ty =
110+
mkFunTy g (mkThisTy g ty) (mkFunTy g (mkRefTupledTy g [ ty; g.IEqualityComparer_ty ]) g.bool_ty)
111+
109112
let mkHashTy g ty =
110113
mkFunTy g (mkThisTy g ty) (mkFunTy g g.unit_ty g.int_ty)
111114

@@ -361,7 +364,7 @@ let mkRecdEquality g tcref (tycon: Tycon) =
361364
thisv, thatv, expr
362365

363366
/// Build the equality implementation for a record type when parameterized by a comparer
364-
let mkRecdEqualityWithComparer g tcref (tycon: Tycon) (_thisv, thise) thatobje (thatv, thate) compe =
367+
let mkRecdEqualityWithComparer g tcref (tycon: Tycon) thise thatobje (thatv, thate) compe isexact =
365368
let m = tycon.Range
366369
let fields = tycon.AllInstanceFieldsAsList
367370
let tinst, ty = mkMinimalTy g tcref
@@ -382,14 +385,21 @@ let mkRecdEqualityWithComparer g tcref (tycon: Tycon) (_thisv, thise) thatobje (
382385
let expr = mkEqualsTestConjuncts g m (List.map mkTest fields)
383386

384387
let expr = mkBindThatAddr g m ty thataddrv thatv thate expr
385-
// will be optimized away if not necessary
386-
let expr = mkIsInstConditional g m ty thatobje thatv expr (mkFalse g m)
388+
389+
let expr =
390+
if isexact then
391+
expr
392+
else
393+
mkIsInstConditional g m ty thatobje thatv expr (mkFalse g m)
387394

388395
let expr =
389396
if tycon.IsStructOrEnumTycon then
390397
expr
391398
else
392-
mkBindThisNullEquals g m thise thatobje expr
399+
if isexact then
400+
mkBindThatNullEquals g m thise thate expr
401+
else
402+
mkBindThisNullEquals g m thise thatobje expr
393403

394404
expr
395405

@@ -425,7 +435,7 @@ let mkExnEquality (g: TcGlobals) exnref (exnc: Tycon) =
425435
thisv, thatv, expr
426436

427437
/// Build the equality implementation for an exception definition when parameterized by a comparer
428-
let mkExnEqualityWithComparer g exnref (exnc: Tycon) (_thisv, thise) thatobje (thatv, thate) compe =
438+
let mkExnEqualityWithComparer g exnref (exnc: Tycon) thise thatobje (thatv, thate) compe isexact =
429439
let m = exnc.Range
430440
let thataddrv, thataddre = mkThatAddrLocal g m g.exn_ty
431441

@@ -453,13 +463,21 @@ let mkExnEqualityWithComparer g exnref (exnc: Tycon) (_thisv, thise) thatobje (t
453463
mbuilder.Close(dtree, m, g.bool_ty)
454464

455465
let expr = mkBindThatAddr g m g.exn_ty thataddrv thatv thate expr
456-
let expr = mkIsInstConditional g m g.exn_ty thatobje thatv expr (mkFalse g m)
466+
467+
let expr =
468+
if isexact then
469+
expr
470+
else
471+
mkIsInstConditional g m g.exn_ty thatobje thatv expr (mkFalse g m)
457472

458473
let expr =
459474
if exnc.IsStructOrEnumTycon then
460475
expr
461476
else
462-
mkBindThisNullEquals g m thise thatobje expr
477+
if isexact then
478+
mkBindThatNullEquals g m thise thate expr
479+
else
480+
mkBindThisNullEquals g m thise thatobje expr
463481

464482
expr
465483

@@ -758,7 +776,7 @@ let mkUnionEquality g tcref (tycon: Tycon) =
758776
thisv, thatv, expr
759777

760778
/// Build the equality implementation for a union type when parameterized by a comparer
761-
let mkUnionEqualityWithComparer g tcref (tycon: Tycon) (_thisv, thise) thatobje (thatv, thate) compe =
779+
let mkUnionEqualityWithComparer g tcref (tycon: Tycon) thise thatobje (thatv, thate) compe isexact =
762780
let m = tycon.Range
763781
let ucases = tycon.UnionCasesAsList
764782
let tinst, ty = mkMinimalTy g tcref
@@ -846,13 +864,21 @@ let mkUnionEqualityWithComparer g tcref (tycon: Tycon) (_thisv, thise) thatobje
846864
(mkCompGenLet m thattagv (mkUnionCaseTagGetViaExprAddr (thataddre, tcref, tinst, m)) tagsEqTested)
847865

848866
let expr = mkBindThatAddr g m ty thataddrv thatv thate expr
849-
let expr = mkIsInstConditional g m ty thatobje thatv expr (mkFalse g m)
867+
868+
let expr =
869+
if isexact then
870+
expr
871+
else
872+
mkIsInstConditional g m ty thatobje thatv expr (mkFalse g m)
850873

851874
let expr =
852875
if tycon.IsStructOrEnumTycon then
853876
expr
854877
else
855-
mkBindThisNullEquals g m thise thatobje expr
878+
if isexact then
879+
mkBindThatNullEquals g m thise thate expr
880+
else
881+
mkBindThisNullEquals g m thise thatobje expr
856882

857883
expr
858884

@@ -1014,6 +1040,15 @@ let getAugmentationAttribs g (tycon: Tycon) =
10141040
TryFindFSharpBoolAttribute g g.attrib_CustomComparisonAttribute tycon.Attribs,
10151041
TryFindFSharpBoolAttribute g g.attrib_StructuralComparisonAttribute tycon.Attribs
10161042

1043+
[<NoEquality; NoComparison; StructuredFormatDisplay("{DebugText}")>]
1044+
type EqualityWithComparerAugmentation =
1045+
{
1046+
GetHashCode: Val
1047+
GetHashCodeWithComparer: Val
1048+
EqualsWithComparer: Val
1049+
EqualsExactWithComparer: Val
1050+
}
1051+
10171052
let CheckAugmentationAttribs isImplementation g amap (tycon: Tycon) =
10181053
let m = tycon.Range
10191054
let attribs = getAugmentationAttribs g tycon
@@ -1333,7 +1368,25 @@ let MakeValsForEqualityWithComparerAugmentation g (tcref: TyconRef) =
13331368
let withcEqualsVal =
13341369
mkValSpec g tcref ty vis (Some(mkIStructuralEquatableEqualsSlotSig g)) "Equals" (tps +-> (mkEqualsWithComparerTy g ty)) tupArg false
13351370

1336-
objGetHashCodeVal, withcGetHashCodeVal, withcEqualsVal
1371+
let withcEqualsValExact =
1372+
mkValSpec
1373+
g
1374+
tcref
1375+
ty
1376+
vis
1377+
// This doesn't implement any interface.
1378+
None
1379+
"Equals"
1380+
(tps +-> (mkEqualsWithComparerTyExact g ty))
1381+
tupArg
1382+
false
1383+
1384+
{
1385+
GetHashCode = objGetHashCodeVal
1386+
GetHashCodeWithComparer = withcGetHashCodeVal
1387+
EqualsWithComparer = withcEqualsVal
1388+
EqualsExactWithComparer = withcEqualsValExact
1389+
}
13371390

13381391
let MakeBindingsForCompareAugmentation g (tycon: Tycon) =
13391392
let tcref = mkLocalTyconRef tycon
@@ -1419,7 +1472,7 @@ let MakeBindingsForEqualityWithComparerAugmentation (g: TcGlobals) (tycon: Tycon
14191472
let mkStructuralEquatable hashf equalsf =
14201473
match tycon.GeneratedHashAndEqualsWithComparerValues with
14211474
| None -> []
1422-
| Some(objGetHashCodeVal, withcGetHashCodeVal, withcEqualsVal) ->
1475+
| Some(objGetHashCodeVal, withcGetHashCodeVal, withcEqualsVal, withcEqualsExactValOption) ->
14231476

14241477
// build the hash rhs
14251478
let withcGetHashCodeExpr =
@@ -1451,12 +1504,33 @@ let MakeBindingsForEqualityWithComparerAugmentation (g: TcGlobals) (tycon: Tycon
14511504

14521505
// build the equals rhs
14531506
let withcEqualsExpr =
1454-
let _tinst, ty = mkMinimalTy g tcref
1507+
let tinst, ty = mkMinimalTy g tcref
14551508
let thisv, thise = mkThisVar g m ty
14561509
let thatobjv, thatobje = mkCompGenLocal m "obj" g.obj_ty
14571510
let thatv, thate = mkCompGenLocal m "that" ty
14581511
let compv, compe = mkCompGenLocal m "comp" g.IEqualityComparer_ty
1459-
let equalse = equalsf g tcref tycon (thisv, thise) thatobje (thatv, thate) compe
1512+
1513+
// if the new overload is available, use it
1514+
// otherwise, generate the whole equals thing
1515+
let equalse =
1516+
match withcEqualsExactValOption with
1517+
| Some withcEqualsExactVal ->
1518+
mkIsInstConditional
1519+
g
1520+
m
1521+
ty
1522+
thatobje
1523+
thatv
1524+
(mkApps
1525+
g
1526+
((exprForValRef m withcEqualsExactVal, withcEqualsExactVal.Type),
1527+
(if isNil tinst then [] else [ tinst ]),
1528+
[ thise; mkRefTupled g m [ thate; compe ] [ty; g.IEqualityComparer_ty ] ],
1529+
m))
1530+
(mkFalse g m)
1531+
| None ->
1532+
equalsf g tcref tycon thise thatobje (thatv, thate) compe false
1533+
14601534
mkMultiLambdas g m tps [ [ thisv ]; [ thatobjv; compv ] ] (equalse, g.bool_ty)
14611535

14621536
let objGetHashCodeExpr =
@@ -1481,9 +1555,22 @@ let MakeBindingsForEqualityWithComparerAugmentation (g: TcGlobals) (tycon: Tycon
14811555

14821556
mkLambdas g m tps [ thisv; unitv ] (hashe, g.int_ty)
14831557

1558+
let withcEqualsExactExpr =
1559+
let _tinst, ty = mkMinimalTy g tcref
1560+
let thisv, thise = mkThisVar g m ty
1561+
let thatv, thate = mkCompGenLocal m "obj" ty
1562+
let compv, compe = mkCompGenLocal m "comp" g.IEqualityComparer_ty
1563+
1564+
let equalse = equalsf g tcref tycon thise thate (thatv, thate) compe true
1565+
1566+
mkMultiLambdas g m tps [ [ thisv ]; [ thatv; compv ] ] (equalse, g.bool_ty)
1567+
14841568
[
14851569
(mkCompGenBind withcGetHashCodeVal.Deref withcGetHashCodeExpr)
14861570
(mkCompGenBind objGetHashCodeVal.Deref objGetHashCodeExpr)
1571+
match withcEqualsExactValOption with
1572+
| Some withcEqualsExactVal -> mkCompGenBind withcEqualsExactVal.Deref withcEqualsExactExpr
1573+
| None -> ()
14871574
(mkCompGenBind withcEqualsVal.Deref withcEqualsExpr)
14881575
]
14891576

src/Compiler/Checking/AugmentWithHashCompare.fsi

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ open FSharp.Compiler
77
open FSharp.Compiler.TypedTree
88
open FSharp.Compiler.TcGlobals
99

10+
[<NoEquality; NoComparison; StructuredFormatDisplay("{DebugText}")>]
11+
type EqualityWithComparerAugmentation =
12+
{ GetHashCode: Val
13+
GetHashCodeWithComparer: Val
14+
EqualsWithComparer: Val
15+
EqualsExactWithComparer: Val }
16+
1017
val CheckAugmentationAttribs: bool -> TcGlobals -> Import.ImportMap -> Tycon -> unit
1118

1219
val TyconIsCandidateForAugmentationWithCompare: TcGlobals -> Tycon -> bool
@@ -21,7 +28,7 @@ val MakeValsForCompareWithComparerAugmentation: TcGlobals -> TyconRef -> Val
2128

2229
val MakeValsForEqualsAugmentation: TcGlobals -> TyconRef -> Val * Val
2330

24-
val MakeValsForEqualityWithComparerAugmentation: TcGlobals -> TyconRef -> Val * Val * Val
31+
val MakeValsForEqualityWithComparerAugmentation: TcGlobals -> TyconRef -> EqualityWithComparerAugmentation
2532

2633
val MakeBindingsForCompareAugmentation: TcGlobals -> Tycon -> Binding list
2734

src/Compiler/Checking/CheckDeclarations.fs

+11-5
Original file line numberDiff line numberDiff line change
@@ -817,12 +817,18 @@ module AddAugmentationDeclarations =
817817
if hasExplicitIStructuralEquatable then
818818
errorR(Error(FSComp.SR.tcImplementsIStructuralEquatableExplicitly(tycon.DisplayName), m))
819819
else
820-
let evspec1, evspec2, evspec3 = AugmentTypeDefinitions.MakeValsForEqualityWithComparerAugmentation g tcref
820+
let augmentation = AugmentTypeDefinitions.MakeValsForEqualityWithComparerAugmentation g tcref
821821
PublishInterface cenv env.DisplayEnv tcref m true g.mk_IStructuralEquatable_ty
822-
tcaug.SetHashAndEqualsWith (mkLocalValRef evspec1, mkLocalValRef evspec2, mkLocalValRef evspec3)
823-
PublishValueDefn cenv env ModuleOrMemberBinding evspec1
824-
PublishValueDefn cenv env ModuleOrMemberBinding evspec2
825-
PublishValueDefn cenv env ModuleOrMemberBinding evspec3
822+
tcaug.SetHashAndEqualsWith (
823+
mkLocalValRef augmentation.GetHashCode,
824+
mkLocalValRef augmentation.GetHashCodeWithComparer,
825+
mkLocalValRef augmentation.EqualsWithComparer,
826+
Some (mkLocalValRef augmentation.EqualsExactWithComparer))
827+
828+
PublishValueDefn cenv env ModuleOrMemberBinding augmentation.GetHashCode
829+
PublishValueDefn cenv env ModuleOrMemberBinding augmentation.GetHashCodeWithComparer
830+
PublishValueDefn cenv env ModuleOrMemberBinding augmentation.EqualsWithComparer
831+
PublishValueDefnMaybeInclCompilerGenerated cenv env true ModuleOrMemberBinding augmentation.EqualsExactWithComparer
826832

827833
let AddGenericCompareBindings (cenv: cenv) (tycon: Tycon) =
828834
if (* AugmentTypeDefinitions.TyconIsCandidateForAugmentationWithCompare cenv.g tycon && *) Option.isSome tycon.GeneratedCompareToValues then

src/Compiler/Checking/MethodOverrides.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ module DispatchSlotChecking =
776776

777777
// Get all the members that are immediately part of this type
778778
// Include the auto-generated members
779-
let allImmediateMembers = tycon.MembersOfFSharpTyconSorted @ tycon.AllGeneratedValues
779+
let allImmediateMembers = tycon.MembersOfFSharpTyconSorted @ tycon.AllGeneratedInterfaceImplsAndOverrides
780780

781781
// Get all the members we have to implement, organized by each type we explicitly implement
782782
let slotImplSets = GetSlotImplSets infoReader denv AccessibleFromSomewhere false allReqdTys

src/Compiler/Checking/NicePrint.fs

+1
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,7 @@ module TastDefinitionPrinting =
19091909
match vrefOpt with
19101910
| None -> true
19111911
| Some vref ->
1912+
(not vref.IsCompilerGenerated) &&
19121913
(denv.showObsoleteMembers || not (CheckFSharpAttributesForObsolete denv.g vref.Attribs)) &&
19131914
(denv.showHiddenMembers || not (CheckFSharpAttributesForHidden denv.g vref.Attribs))
19141915

src/Compiler/Checking/PostInferenceChecks.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2280,7 +2280,7 @@ let CheckEntityDefn cenv env (tycon: Entity) =
22802280
else MethInfosEquivByNameAndPartialSig eraseFlag true g cenv.amap m minfo minfo2 (* partial ignores return type *)
22812281

22822282
let immediateMeths =
2283-
[ for v in tycon.AllGeneratedValues do yield FSMeth (g, ty, v, None)
2283+
[ for v in tycon.AllGeneratedInterfaceImplsAndOverrides do yield FSMeth (g, ty, v, None)
22842284
yield! GetImmediateIntrinsicMethInfosOfType (None, AccessibleFromSomewhere) g cenv.amap m ty ]
22852285

22862286
let immediateProps = GetImmediateIntrinsicPropInfosOfType (None, AccessibleFromSomewhere) g cenv.amap m ty

src/Compiler/CodeGen/IlxGen.fs

+8-2
Original file line numberDiff line numberDiff line change
@@ -2189,7 +2189,7 @@ type AnonTypeGenerationTable() =
21892189

21902190
let vspec1, vspec2 = AugmentTypeDefinitions.MakeValsForEqualsAugmentation g tcref
21912191

2192-
let evspec1, evspec2, evspec3 =
2192+
let augmentation =
21932193
AugmentTypeDefinitions.MakeValsForEqualityWithComparerAugmentation g tcref
21942194

21952195
let cvspec1, cvspec2 = AugmentTypeDefinitions.MakeValsForCompareAugmentation g tcref
@@ -2200,7 +2200,13 @@ type AnonTypeGenerationTable() =
22002200
tcaug.SetCompare(mkLocalValRef cvspec1, mkLocalValRef cvspec2)
22012201
tcaug.SetCompareWith(mkLocalValRef cvspec3)
22022202
tcaug.SetEquals(mkLocalValRef vspec1, mkLocalValRef vspec2)
2203-
tcaug.SetHashAndEqualsWith(mkLocalValRef evspec1, mkLocalValRef evspec2, mkLocalValRef evspec3)
2203+
2204+
tcaug.SetHashAndEqualsWith(
2205+
mkLocalValRef augmentation.GetHashCode,
2206+
mkLocalValRef augmentation.GetHashCodeWithComparer,
2207+
mkLocalValRef augmentation.EqualsWithComparer,
2208+
Some(mkLocalValRef augmentation.EqualsExactWithComparer)
2209+
)
22042210

22052211
// Build the ILTypeDef. We don't rely on the normal record generation process because we want very specific field names
22062212

0 commit comments

Comments
 (0)