From d6a575b96b4b8738b46201a77cfb8efccb6054d7 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 16 Jan 2024 17:19:36 +0100 Subject: [PATCH 01/17] Docs on equality in F# --- docs/equality.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/equality.md diff --git a/docs/equality.md b/docs/equality.md new file mode 100644 index 00000000000..6c873d7ecf2 --- /dev/null +++ b/docs/equality.md @@ -0,0 +1,126 @@ +# Compiling Equality and Comparison + +## How we compile equality "a = b" + +This very much depends on the type involved in the equality as known by the compiler + +Aim here is to flesh these all out with +* **Semantics**: what semantics the user expects, and what the semantics actually is +* **Perf expectation**: what perf the user expects +* **Compilation today**: How we actually compile today, with sharplab.io link +* **Perf today**: What is the perf we achieve today +* **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure change + +### primitive type (int32, int64, ...) + +```fsharp +let f (x: int) (y: int) = (x = y) +``` + +* Semantics: equality on primitive (PER for floating point) +* Perf: User expects full performance down to native +* Compilation today: compiles to IL instruction ✅ +* Perf today: good ✅ + +### tuple type + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks +* Compilation today: tuple equality is flattened to constituent checks up to size 5 ✅ +* Perf today: ok up to size 5 ✅ + +### struct tuple type + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks +* Compilation today: CHECK ME ❔ +* Perf today: CHECK ME ❔ + +### array type (byte[], int[], some-struct-type[], ...) + +* Semantics: User expects structural +* Perf: User expects perf is sum of constituent parts +* Compilation today: GenericEqualityIntrinsic +* Perf today: this is hand-optimized for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ +* Compilation after 5112, either `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` or `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` +* Perf after 5112: CHECK ME ❔ + +### C# or F# enum type + +* Semantics: User expects identical to equality on underlying +* Perf: User expects flattening to underlying type +* Compilation today: CHECK ME ❔ + +### C# struct type + +* Semantics: User really expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: always boxes (❌, Problem1) + +### F# struct type (with compiler-generated structural equality) + +* Semantics: User expects field-by-field structural equality with no boxing +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: always boxes (❌, Problem1) + +### F# large ref record/union type + +Here "large" means the compiler-generated structural equality is not inlined + +* Semantics: User expects structural by default +* Perf: User expects perf is sum of constituent parts, type-specialized if generic +* Today: direct call to `Equals(T)`, which has specialized code but boxes fields if struct or T, see Problem1, Problem2 ❌ + +### F# tiny ref record/union type + +Here "tiny" means the compiler-generated structural equality IS inlined + +* Semantics: User expects structural by default +* Perf: User expects perf is sum of constituent parts, type-specialized if generic +* Test: Equals06.fsx +* Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields +* Perf today: boxes on struct and generic fields, see Problem1, Problem2 +* 5112: `FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)` on struct and generic fields + +### Any ref type supporting IEquatable + +* Semantics: User expects calling `IEquatable` implementation is used, actual is call to `this.Equals(that)`. These are generally identical semantics + +### ref type only supporting .Equals(object) override + +* Semantics: User expects call to this override, perhaps a non-virtual call + +### other ref type + +* Semantics: User expects fast reference equality +* Compilation today: + +### Generic `'T` in non-inlined generic code + +* Semantics: User expects the PER equality semantics of whatever T actually is +* Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) +* Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined +* Compilation today: GenericEqualityERIntrinsic (❌,boxes) +* Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) + +### T in inlined generic code + +* Semantics: User expects perf same as type specialized code (✅) + +## Techniques available to us + +1. Flatten and inline +2. RCG: Use reflective code generation internally in FSharp.Core +3. KFS: Rely on known semantics of F# structural types and treat those as special +4. TS: Hand-code type-specializations using static optimization conditions in FSharp.Core +5. TT: Type-indexed tables of baked (poss by reflection) equality comparers and functions, where some pre-computation is done +6. DV: De-virtualization +7. DEQ: Use EqualityComparer<'T>.Default where possible + +## Previous attempts + +### 5112 https://github.com/dotnet/fsharp/pull/5112 + +* Uses TT, DEQ, KFS, DV +* Focuses on solving Problem2 +* Not breaking \ No newline at end of file From 8cc6bc2307626af66ae32934fcdba3502b818008 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 17 Jan 2024 16:50:08 +0000 Subject: [PATCH 02/17] Update equality.md --- docs/equality.md | 90 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 6c873d7ecf2..af75864c707 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -10,8 +10,9 @@ Aim here is to flesh these all out with * **Compilation today**: How we actually compile today, with sharplab.io link * **Perf today**: What is the perf we achieve today * **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure change +* **sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab -### primitive type (int32, int64, ...) +### primitive integer types (int32, int64, ...) ```fsharp let f (x: int) (y: int) = (x = y) @@ -21,20 +22,52 @@ let f (x: int) (y: int) = (x = y) * Perf: User expects full performance down to native * Compilation today: compiles to IL instruction ✅ * Perf today: good ✅ +* [sharplab int32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxAwEsA7NASlwE9DSKMBeXPRjK8gWACgg===) -### tuple type +### primitive floating point types (float32, float64) + +```fsharp +let f (x: float32) (y: float32) = (x = y) +``` + +* Semantics: IEEE floating point equality (respecting NaN etc.) +* Perf: User expects full performance down to native +* Compilation today: compiles to IL instruction ✅ +* Perf today: good ✅ +* [sharplab float32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxC2AHsBDNAZgCYBKXAT0LBPOroF5c8NP6aBYAFBA===) + +### primitive string, decimal + +* Semantics: .NET equivalent equality +* Perf: User expects full performance down to native +* Compilation today: compiles to `String.Equals` or `Decimal.op_Equality` call ✅ +* Perf today: good ✅ +* [sharplab decimal](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h7LYDG8AtgIbACUxAnuZTQ83gLzEk+eVkwCwAKCA) +* [sharplab string](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h4QwBO8AdgOYCUxAnuZTQ8wLzEl68WjALAAoIA==) + +### tuple type (size <= 5) * Semantics: User expects structural * Perf: User expects flattening to constituent checks * Compilation today: tuple equality is flattened to constituent checks up to size 5 ✅ * Perf today: ok up to size 5 ✅ - -### struct tuple type +* [sharplab (int * double * 'T), with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) + +### tuple type (size > 5) * Semantics: User expects structural * Perf: User expects flattening to constituent checks -* Compilation today: CHECK ME ❔ -* Perf today: CHECK ME ❔ +* Compilation today: compiled to GenericEqualityIntrinsic +* Perf today: boxes, does type tests, does virtual calls via IStructuralEqualityComparer etc. ❌(Problem3) +* [sharplab for size 6, with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVI0841MimqyigSidQE9UBeLbL9p+EA==) + +### struct tuple type + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks, or at least the same optimizations as tuples +* Compilation today: compiled to GenericEqualityIntrinsic +* Perf today: boxes, does type tests, does virtual calls via IStructuralEqualityComparer etc. ❌(Problem4) +* [sharplab for size 3](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lRZAJwFcBjNTASwDs0AqVG+x2gSldQE9UBeLbXl1bwgA=) ### array type (byte[], int[], some-struct-type[], ...) @@ -42,26 +75,34 @@ let f (x: int) (y: int) = (x = y) * Perf: User expects perf is sum of constituent parts * Compilation today: GenericEqualityIntrinsic * Perf today: this is hand-optimized for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ +* [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) + +Effect of 5112: * Compilation after 5112, either `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` or `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` * Perf after 5112: CHECK ME ❔ ### C# or F# enum type * Semantics: User expects identical to equality on underlying -* Perf: User expects flattening to underlying type -* Compilation today: CHECK ME ❔ +* Perf: User expects same perf as flattening to underlying type +* Compilation today: flattens to underlying type +* Perf today: good ✅ +* [sharplab for C# enum int](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIYUDyYA6ppgNYCUOCjgC8hIqKH90QA===) +* [sharplab for F# enum int](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUDAngBwKYAIBRfAXn3X0qXwEFT8BGCq/AIXoCZ11hcZ8w+ABQAPEEQCU+TPVH1ME9EA=) ### C# struct type * Semantics: User really expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same * Compilation today: `GenericEqualityIntrinsic` * Perf today: always boxes (❌, Problem1) +* [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) ### F# struct type (with compiler-generated structural equality) * Semantics: User expects field-by-field structural equality with no boxing * Compilation today: `GenericEqualityIntrinsic` * Perf today: always boxes (❌, Problem1) +* [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA) ### F# large ref record/union type @@ -103,10 +144,24 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Compilation today: GenericEqualityERIntrinsic (❌,boxes) * Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) -### T in inlined generic code +### Generic `'T` in inlined generic code * Semantics: User expects perf same as type specialized code (✅) +### Generic `'T` in recursive position in structural comparison + +This case happens in structural equality for tuple types and other structural types + +For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) + +* Semantics: User expects the PER equality semantics of whatever T actually is +* Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) +* Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` +* Perf today: boxes +* Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) // TODO: check ?? +* Perf after 5112: TBD, but much better, no boxing in many cases + + ## Techniques available to us 1. Flatten and inline @@ -123,4 +178,19 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Uses TT, DEQ, KFS, DV * Focuses on solving Problem2 -* Not breaking \ No newline at end of file +* Not breaking + +Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an IEqualityComparer and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. We can simply change the definition in the library like this: + +```fsharp + +let GenericEqualityIntrinsic (x : 'T) (y : 'T) : bool = + FSharpEqualityComparer_PER<'T>.EqualityComparer.Equals(x,y) + +let GenericEqualityERIntrinsic (x : 'T) (y : 'T) : bool = + FSharpEqualityComparer_ER<'T>.EqualityComparer.Equals(x,y) + +let GenericHashIntrinsic input = + FSharpEqualityComparer_PER<'T>.EqualityComparer.Hash(input) +``` + From bdcd485d35ec170be1198e70f9cb459b34a16046 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 17 Jan 2024 16:54:08 +0000 Subject: [PATCH 03/17] Update equality.md --- docs/equality.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/equality.md b/docs/equality.md index af75864c707..730b830c611 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -101,9 +101,46 @@ Effect of 5112: * Semantics: User expects field-by-field structural equality with no boxing * Compilation today: `GenericEqualityIntrinsic` -* Perf today: always boxes (❌, Problem1) +* Perf today: always boxes (❌, Problem1b) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA) +Note: the optimization path is a bit strange here, the reductions are: + +```fsharp + +(x = y) + +--inline--> + +GenericEquality x y + +--inline--> + +GenericEqualityFast x y + +--inline--> + +GenericEqualityIntrinsic x y + +--devirtualize--> + +x.Equals(box y, LanguagePrimitives.GenericEqualityComparer); +``` + +The struct type has these generated methods: +```csharp + override bool Equals(object y) + override bool Equals(SomeStruct obj) + override bool Equals(object obj, IEqualityComparer comp) //withcEqualsVal +``` + +These call each other in sequence, boing then bunboxing then boxing. We do NOT generate this method, we probably should: +```csharp + override bool Equals(SomeStruct obj, IEqualityComparer comp) //withcEqualsValUnboxed +``` + +If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing.] + ### F# large ref record/union type Here "large" means the compiler-generated structural equality is not inlined @@ -194,3 +231,6 @@ let GenericHashIntrinsic input = FSharpEqualityComparer_PER<'T>.EqualityComparer.Hash(input) ``` + +``` + From 6780951dd5cf521fb80274b86fcff0acebce3dc5 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 18 Jan 2024 11:11:53 +0000 Subject: [PATCH 04/17] Update equality.md --- docs/equality.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 730b830c611..5cf027b039c 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -18,7 +18,7 @@ Aim here is to flesh these all out with let f (x: int) (y: int) = (x = y) ``` -* Semantics: equality on primitive (PER for floating point) +* Semantics: equality on primitive * Perf: User expects full performance down to native * Compilation today: compiles to IL instruction ✅ * Perf today: good ✅ @@ -38,7 +38,7 @@ let f (x: float32) (y: float32) = (x = y) ### primitive string, decimal -* Semantics: .NET equivalent equality +* Semantics: .NET equivalent equality, non-localized for strings * Perf: User expects full performance down to native * Compilation today: compiles to `String.Equals` or `Decimal.op_Equality` call ✅ * Perf today: good ✅ @@ -58,13 +58,13 @@ let f (x: float32) (y: float32) = (x = y) * Semantics: User expects structural * Perf: User expects flattening to constituent checks * Compilation today: compiled to GenericEqualityIntrinsic -* Perf today: boxes, does type tests, does virtual calls via IStructuralEqualityComparer etc. ❌(Problem3) -* [sharplab for size 6, with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVI0841MimqyigSidQE9UBeLbL9p+EA==) +* Perf today: size > 5 is not flattened. This means the check does type tests, does virtual calls via IStructuralEqualityComparer, boxes etc. ❌(Problem3) +* [sharplab for size 6](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVI0841MimqyigSidQE9UBeLbL9p+EA==) ### struct tuple type * Semantics: User expects structural -* Perf: User expects flattening to constituent checks, or at least the same optimizations as tuples +* Perf: User expects flattening to constituent checks, **or at least the same optimizations as tuples** * Compilation today: compiled to GenericEqualityIntrinsic * Perf today: boxes, does type tests, does virtual calls via IStructuralEqualityComparer etc. ❌(Problem4) * [sharplab for size 3](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lRZAJwFcBjNTASwDs0AqVG+x2gSldQE9UBeLbXl1bwgA=) @@ -79,6 +79,7 @@ let f (x: float32) (y: float32) = (x = y) Effect of 5112: * Compilation after 5112, either `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` or `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` +* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized * Perf after 5112: CHECK ME ❔ ### C# or F# enum type @@ -158,7 +159,10 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Test: Equals06.fsx * Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields * Perf today: boxes on struct and generic fields, see Problem1, Problem2 + +Effect of 5112: * 5112: `FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)` on struct and generic fields +* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized ### Any ref type supporting IEquatable @@ -179,7 +183,10 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined * Compilation today: GenericEqualityERIntrinsic (❌,boxes) + +Effect of 5112: * Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) +* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized ### Generic `'T` in inlined generic code @@ -195,9 +202,11 @@ For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQC * Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` * Perf today: boxes + +Effect of 5112: * Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) // TODO: check ?? * Perf after 5112: TBD, but much better, no boxing in many cases - +* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized ## Techniques available to us @@ -208,8 +217,8 @@ For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQC 5. TT: Type-indexed tables of baked (poss by reflection) equality comparers and functions, where some pre-computation is done 6. DV: De-virtualization 7. DEQ: Use EqualityComparer<'T>.Default where possible - -## Previous attempts + +## Notes on previous attempts to improve things ### 5112 https://github.com/dotnet/fsharp/pull/5112 From ab1a368d421c361003fac2e26ec307e743238265 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 8 Feb 2024 16:45:20 +0000 Subject: [PATCH 05/17] Update equality.md --- docs/equality.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/equality.md b/docs/equality.md index 5cf027b039c..6e3527dae42 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -1,5 +1,74 @@ # Compiling Equality and Comparison +This spec covers how equality is compiled and executed by the F# compiler and library, based mainly on the types involved in the equality operation after all inlining, type specialization and other optimizations have been applied. + +### What do we mean by an equality operation? + +This affects the semantics and performand of the following coding constructs + +* `a = b` where each element has some type `EQTYPE` +* `a <> b` where each element has some type `EQTYPE` + +In addition, the performance also affects uses of the following FSharp.Core constructs which, after inlining, genrate code that contains an equality check at the specific `EQTYPE` + +Inlined constructs (only resulting in naked generic equality if themselves used from a non-inlined generic context) +* `HashIdentity.Structural` +* `Array.contains<'T> x array`, EQTYPE is inlined 'T, results in specialised equality +* `Array.countBy` likewise +* `List.contains` likewise +* `Seq.contains` likewise +* ... some more here ... + +Non-inlined constructs always resulting in naked generic equality +* `Array.groupBy<'Key, 'T> f array`, `, EQTYPE is non-inlined 'Key, results in naked generic equality +* `Array.distinct array` likewise +* `Array.distinctBy array` likewise +* `Array.except array` likewise +* `List.countBy` likewise +* `List.distinct` likewise +* `List.distinctBy` likewise +* `List.except` likewise +* `List.groupBy` likewise +* `Seq.countBy` likewise +* `Seq.distinct` likewise +* `Seq.distinctBy` likewise +* `Seq.except` likewise +* `Seq.groupBy` likewise +* ... may be more here, see `: equality` constraints in FSharp.Core for non-inlined code. + +### What is the type known to the compiler and library for an equality operation? + +Example 1: + +```fsharp +let x = HashIdentity.Structural // EQTYPE known to compiler is `byte` +``` + +Example 2: a "naked" generic context +```fsharp +let f<'T> () = + ... some long code + // EQTYPE known to the compiler is `'T` + // RUNTIME-EQTYPE known to the library is `byte` + let x = HashIdentity.Structural<'T> + ... some long code + +f() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE +``` + +Example 2: a struct type +```fsharp +let f<'T> () = + ... some long code + // EQTYPE known to the compiler is `SomeStructType<'T>` + // RUNTIME-EQTYPE known to the library is `SomeStructType` + let x = HashIdentity.Structural> + ... some long code + +f() // performance of this determined by EQTYPE> and RUNTIME-EQTYPE> +``` + + ## How we compile equality "a = b" This very much depends on the type involved in the equality as known by the compiler From 7b04b6e8d8bc256099b38f55b157317523d8abcf Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 8 Feb 2024 16:55:32 +0000 Subject: [PATCH 06/17] Update equality.md --- docs/equality.md | 78 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 6e3527dae42..a1427653227 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -4,24 +4,47 @@ This spec covers how equality is compiled and executed by the F# compiler and li ### What do we mean by an equality operation? -This affects the semantics and performand of the following coding constructs +This spec is about the semantics and performance of the following coding constructs -* `a = b` where each element has some type `EQTYPE` -* `a <> b` where each element has some type `EQTYPE` +* `a = b` +* `a <> b` -In addition, the performance also affects uses of the following FSharp.Core constructs which, after inlining, genrate code that contains an equality check at the specific `EQTYPE` +It is also about the semantics and performance of uses of the following FSharp.Core constructs. -Inlined constructs (only resulting in naked generic equality if themselves used from a non-inlined generic context) -* `HashIdentity.Structural` -* `Array.contains<'T> x array`, EQTYPE is inlined 'T, results in specialised equality -* `Array.countBy` likewise -* `List.contains` likewise -* `Seq.contains` likewise -* ... some more here ... +which, after inlining, genrate code that contains an equality check at the specific `EQTYPE` +* `HashIdentity.Structural<;T>` +* `{Array,Seq,List}.contains` +* `{Array,Seq,List}.countBy` +* `{Array,Seq,List}.groupBy` +* `{Array,Seq,List}.distinct` +* `{Array,Seq,List}.distinctBy` +* `{Array,Seq,List}.except` -Non-inlined constructs always resulting in naked generic equality +All of which have implied equality checks. Some of these operations are inlined, see below, which in turn affects the semantics and performance of the overall operation. + +### What is the type known to the compiler and library for an equality operation? + +The static type known to the F# compiler is crucial to determining the performance of the operation. The runtime type of the equality check is also significant in some situations. + +Here we define the relevant static type `EQTYPE` for the different constructs above: + +#### Basics + +* `a = b`: `EQTYPE` is the statically known type of `a` or `b` +* `a <> b` `EQTYPE` is the statically known type of `a` or `b` + +#### Inlined constructs +* `HashIdentity.Structural``, EQTYPE is the **inlined** 'T (results in specialised equality) +* `Array.contains`, EQTYPE is the **inlined** 'T (results in specialised equality) +* `Array.countBy``, EQTYPE is the **inlined** 'Key (results in specialised equality) +* `List.contains` likewise +* `Seq.contains` likewise + +These only resulting in naked generic equality if themselves used from a non-inlined generic context. + +#### Non-inlined constructs always resulting in naked generic equality * `Array.groupBy<'Key, 'T> f array`, `, EQTYPE is non-inlined 'Key, results in naked generic equality -* `Array.distinct array` likewise +* `Array.distinct<'T> array` likewise for T * `Array.distinctBy array` likewise * `Array.except array` likewise * `List.countBy` likewise @@ -36,7 +59,8 @@ Non-inlined constructs always resulting in naked generic equality * `Seq.groupBy` likewise * ... may be more here, see `: equality` constraints in FSharp.Core for non-inlined code. -### What is the type known to the compiler and library for an equality operation? +These **always** result in naked generic equality checks. + Example 1: @@ -44,34 +68,46 @@ Example 1: let x = HashIdentity.Structural // EQTYPE known to compiler is `byte` ``` -Example 2: a "naked" generic context +Example 2: a non-inlined "naked" generic context ```fsharp -let f<'T> () = +let f2<'T> () = ... some long code // EQTYPE known to the compiler is `'T` // RUNTIME-EQTYPE known to the library is `byte` let x = HashIdentity.Structural<'T> ... some long code -f() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE +f2() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE +``` + +Example 3: an inlined generic context +```fsharp +let f3<'T> () = + ... some long code + // EQTYPE known to the compiler is `byte` + // RUNTIME-EQTYPE known to the library is `byte` + let x = HashIdentity.Structural<'T> + ... some long code + +f3() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE ``` -Example 2: a struct type +Example 4: a generic struct type in a non-inline generic context ```fsharp -let f<'T> () = +let f4<'T> () = ... some long code // EQTYPE known to the compiler is `SomeStructType<'T>` // RUNTIME-EQTYPE known to the library is `SomeStructType` let x = HashIdentity.Structural> ... some long code -f() // performance of this determined by EQTYPE> and RUNTIME-EQTYPE> +f4() // performance of this determined by EQTYPE> and RUNTIME-EQTYPE> ``` ## How we compile equality "a = b" -This very much depends on the type involved in the equality as known by the compiler +This very much depends on the `EQTYPE` involved in the equality as known by the compiler Aim here is to flesh these all out with * **Semantics**: what semantics the user expects, and what the semantics actually is From aee0d4f26036f082b847b841c16d908c4983c774 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 9 Feb 2024 14:29:28 +0100 Subject: [PATCH 07/17] Update equality.md --- docs/equality.md | 88 +++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index a1427653227..44bc22a8093 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -9,10 +9,10 @@ This spec is about the semantics and performance of the following coding constru * `a = b` * `a <> b` -It is also about the semantics and performance of uses of the following FSharp.Core constructs. +It is also about the semantics and performance of uses of the following `FSharp.Core` constructs. -which, after inlining, genrate code that contains an equality check at the specific `EQTYPE` -* `HashIdentity.Structural<;T>` +which, after inlining, generate code that contains an equality check at the specific `EQTYPE` +* `HashIdentity.Structural<'T>` * `{Array,Seq,List}.contains` * `{Array,Seq,List}.countBy` * `{Array,Seq,List}.groupBy` @@ -31,44 +31,46 @@ Here we define the relevant static type `EQTYPE` for the different constructs ab #### Basics * `a = b`: `EQTYPE` is the statically known type of `a` or `b` -* `a <> b` `EQTYPE` is the statically known type of `a` or `b` +* `a <> b`: `EQTYPE` is the statically known type of `a` or `b` -#### Inlined constructs -* `HashIdentity.Structural``, EQTYPE is the **inlined** 'T (results in specialised equality) -* `Array.contains`, EQTYPE is the **inlined** 'T (results in specialised equality) -* `Array.countBy``, EQTYPE is the **inlined** 'Key (results in specialised equality) +#### Inlined constructs + +* `HashIdentity.Structural<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) +* `Array.contains<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) * `List.contains` likewise * `Seq.contains` likewise These only resulting in naked generic equality if themselves used from a non-inlined generic context. #### Non-inlined constructs always resulting in naked generic equality -* `Array.groupBy<'Key, 'T> f array`, `, EQTYPE is non-inlined 'Key, results in naked generic equality -* `Array.distinct<'T> array` likewise for T + +* `Array.groupBy<'Key, 'T> f array`, EQTYPE is non-inlined 'Key, results in naked generic equality +* `Array.countBy array` likewise for `'T` +* `Array.distinct<'T> array` likewise * `Array.distinctBy array` likewise * `Array.except array` likewise +* `List.groupBy` likewise * `List.countBy` likewise * `List.distinct` likewise * `List.distinctBy` likewise * `List.except` likewise -* `List.groupBy` likewise +* `Seq.groupBy` likewise * `Seq.countBy` likewise * `Seq.distinct` likewise * `Seq.distinctBy` likewise * `Seq.except` likewise -* `Seq.groupBy` likewise -* ... may be more here, see `: equality` constraints in FSharp.Core for non-inlined code. +* ... may be more here, see `: equality` constraints in `FSharp.Core` for non-inlined code. These **always** result in naked generic equality checks. - Example 1: ```fsharp let x = HashIdentity.Structural // EQTYPE known to compiler is `byte` ``` -Example 2: a non-inlined "naked" generic context +Example 2 (a non-inlined "naked" generic context): + ```fsharp let f2<'T> () = ... some long code @@ -80,7 +82,8 @@ let f2<'T> () = f2() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE ``` -Example 3: an inlined generic context +Example 3 (an inlined generic context): + ```fsharp let f3<'T> () = ... some long code @@ -92,7 +95,8 @@ let f3<'T> () = f3() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE ``` -Example 4: a generic struct type in a non-inline generic context +Example 4 (a generic struct type in a non-inline generic context): + ```fsharp let f4<'T> () = ... some long code @@ -104,12 +108,11 @@ let f4<'T> () = f4() // performance of this determined by EQTYPE> and RUNTIME-EQTYPE> ``` - ## How we compile equality "a = b" This very much depends on the `EQTYPE` involved in the equality as known by the compiler -Aim here is to flesh these all out with +Aim here is to flesh these all out with: * **Semantics**: what semantics the user expects, and what the semantics actually is * **Perf expectation**: what perf the user expects * **Compilation today**: How we actually compile today, with sharplab.io link @@ -117,7 +120,7 @@ Aim here is to flesh these all out with * **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure change * **sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab -### primitive integer types (int32, int64, ...) +### primitive integer types (`int32`, `int64`, ...) ```fsharp let f (x: int) (y: int) = (x = y) @@ -129,7 +132,7 @@ let f (x: int) (y: int) = (x = y) * Perf today: good ✅ * [sharplab int32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxAwEsA7NASlwE9DSKMBeXPRjK8gWACgg===) -### primitive floating point types (float32, float64) +### primitive floating point types (`float32`, `float64`) ```fsharp let f (x: float32) (y: float32) = (x = y) @@ -141,7 +144,7 @@ let f (x: float32) (y: float32) = (x = y) * Perf today: good ✅ * [sharplab float32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxC2AHsBDNAZgCYBKXAT0LBPOroF5c8NP6aBYAFBA===) -### primitive string, decimal +### primitive `string`, `decimal` * Semantics: .NET equivalent equality, non-localized for strings * Perf: User expects full performance down to native @@ -182,10 +185,10 @@ let f (x: float32) (y: float32) = (x = y) * Perf today: this is hand-optimized for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ * [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) -Effect of 5112: -* Compilation after 5112, either `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` or `FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)` -* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized -* Perf after 5112: CHECK ME ❔ +Effect of implementing (#5112): +* Compilation after #5112, either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` +* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +* Perf after #5112: ❔ ### C# or F# enum type @@ -241,6 +244,7 @@ The struct type has these generated methods: ``` These call each other in sequence, boing then bunboxing then boxing. We do NOT generate this method, we probably should: + ```csharp override bool Equals(SomeStruct obj, IEqualityComparer comp) //withcEqualsValUnboxed ``` @@ -265,11 +269,11 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields * Perf today: boxes on struct and generic fields, see Problem1, Problem2 -Effect of 5112: -* 5112: `FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)` on struct and generic fields -* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of #5112: +* #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields +* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized -### Any ref type supporting IEquatable +### Any ref type supporting `IEquatable` * Semantics: User expects calling `IEquatable` implementation is used, actual is call to `this.Equals(that)`. These are generally identical semantics @@ -280,7 +284,6 @@ Effect of 5112: ### other ref type * Semantics: User expects fast reference equality -* Compilation today: ### Generic `'T` in non-inlined generic code @@ -289,9 +292,9 @@ Effect of 5112: * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined * Compilation today: GenericEqualityERIntrinsic (❌,boxes) -Effect of 5112: -* Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) -* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of #5112: +* Compilation after #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` +* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized ### Generic `'T` in inlined generic code @@ -308,10 +311,10 @@ For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQC * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` * Perf today: boxes -Effect of 5112: -* Compilation after 5112: FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...) // TODO: check ?? -* Perf after 5112: TBD, but much better, no boxing in many cases -* NOTE: Proposed adjustment to 5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized +Effect of #5112: +* Compilation after #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` +* Perf after #5112: TBD, but much better, no boxing in many cases +* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized ## Techniques available to us @@ -325,7 +328,7 @@ Effect of 5112: ## Notes on previous attempts to improve things -### 5112 https://github.com/dotnet/fsharp/pull/5112 +### #5112 * Uses TT, DEQ, KFS, DV * Focuses on solving Problem2 @@ -334,7 +337,6 @@ Effect of 5112: Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an IEqualityComparer and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. We can simply change the definition in the library like this: ```fsharp - let GenericEqualityIntrinsic (x : 'T) (y : 'T) : bool = FSharpEqualityComparer_PER<'T>.EqualityComparer.Equals(x,y) @@ -343,8 +345,4 @@ let GenericEqualityERIntrinsic (x : 'T) (y : 'T) : bool = let GenericHashIntrinsic input = FSharpEqualityComparer_PER<'T>.EqualityComparer.Hash(input) -``` - - -``` - +``` \ No newline at end of file From c2228f018faa35400fbdc50df4ddc881a1e3b475 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 9 Feb 2024 14:31:03 +0100 Subject: [PATCH 08/17] Update equality.md --- docs/equality.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 44bc22a8093..22761694317 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -185,10 +185,10 @@ let f (x: float32) (y: float32) = (x = y) * Perf today: this is hand-optimized for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ * [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) -Effect of implementing (#5112): -* Compilation after #5112, either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` -* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized -* Perf after #5112: ❔ +Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): +* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` +* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): ❔ ### C# or F# enum type @@ -269,9 +269,9 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields * Perf today: boxes on struct and generic fields, see Problem1, Problem2 -Effect of #5112: -* #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields -* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): +* [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields +* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized ### Any ref type supporting `IEquatable` @@ -292,9 +292,9 @@ Effect of #5112: * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined * Compilation today: GenericEqualityERIntrinsic (❌,boxes) -Effect of #5112: -* Compilation after #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` -* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): +* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` +* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized ### Generic `'T` in inlined generic code @@ -311,10 +311,10 @@ For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQC * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` * Perf today: boxes -Effect of #5112: -* Compilation after #5112: ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` -* Perf after #5112: TBD, but much better, no boxing in many cases -* NOTE: Proposed adjustment to #5112 noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized +Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): +* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` +* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): TBD, but much better, no boxing in many cases +* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized ## Techniques available to us @@ -328,7 +328,7 @@ Effect of #5112: ## Notes on previous attempts to improve things -### #5112 +### [#5112](https://github.com/dotnet/fsharp/pull/5112) * Uses TT, DEQ, KFS, DV * Focuses on solving Problem2 From d4badd48a1fa51cf7f49b1a581b6f6c62eb4979e Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 12 Feb 2024 16:17:23 +0100 Subject: [PATCH 09/17] ER x PER --- docs/equality.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 22761694317..7eb65cbccad 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -2,7 +2,7 @@ This spec covers how equality is compiled and executed by the F# compiler and library, based mainly on the types involved in the equality operation after all inlining, type specialization and other optimizations have been applied. -### What do we mean by an equality operation? +## What do we mean by an equality operation? This spec is about the semantics and performance of the following coding constructs @@ -22,18 +22,34 @@ which, after inlining, generate code that contains an equality check at the spec All of which have implied equality checks. Some of these operations are inlined, see below, which in turn affects the semantics and performance of the overall operation. -### What is the type known to the compiler and library for an equality operation? +## ER vs PER equality + +In math, a (binary) relation is a way to describe a relationship between the elements of sets. "Greater than" is a relation for numbers, "Subset of" is a relation for sets. + +Here we talk about 3 particular relations: +1) Reflexivity - every element is related to itself +- For integers, `=` is reflexive (`a = a` is always true) and `>` is not (`a > a` is never true) +2) Symmetry - if `a` is related to `b`, then `b` is related to `a` +- For integers, `=` is symmetric (`a = b` -> `b = a`) and `>` is not (if `a > b` then `b > a` is false) +3) Transitivity - if `a` is related to `b`, and `b` is related to `c`, then `a` is also related `c` +- For integers, `>` is transitive (`a > b` && `b > c` -> `a > c`) and `√` is not (`a = √b` && `b = √c` doesn't mean `a = √c`) + +If a relation has 1, 2, and 3, we talk about Equivalence Relation (ER). If a relation only has 2 and 3, we talk about Partial Equivalence Relation (PER). + +This matters in comparing floats since they include [NaN](https://en.wikipedia.org/wiki/NaN). Depending on if we consider `NaN = NaN` true or false, we talk about ER or PER comparison respectively. + +## What is the type known to the compiler and library for an equality operation? The static type known to the F# compiler is crucial to determining the performance of the operation. The runtime type of the equality check is also significant in some situations. Here we define the relevant static type `EQTYPE` for the different constructs above: -#### Basics +### Basics * `a = b`: `EQTYPE` is the statically known type of `a` or `b` * `a <> b`: `EQTYPE` is the statically known type of `a` or `b` -#### Inlined constructs +### Inlined constructs * `HashIdentity.Structural<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) * `Array.contains<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) @@ -42,7 +58,7 @@ Here we define the relevant static type `EQTYPE` for the different constructs ab These only resulting in naked generic equality if themselves used from a non-inlined generic context. -#### Non-inlined constructs always resulting in naked generic equality +### Non-inlined constructs always resulting in naked generic equality * `Array.groupBy<'Key, 'T> f array`, EQTYPE is non-inlined 'Key, results in naked generic equality * `Array.countBy array` likewise for `'T` From 9912e0c5b2d3c2c2c9118553be7bc4bbcb35ac78 Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 12 Feb 2024 16:23:40 +0100 Subject: [PATCH 10/17] up --- docs/equality.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/equality.md b/docs/equality.md index 7eb65cbccad..3cd9b2cc923 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -198,7 +198,7 @@ let f (x: float32) (y: float32) = (x = y) * Semantics: User expects structural * Perf: User expects perf is sum of constituent parts * Compilation today: GenericEqualityIntrinsic -* Perf today: this is hand-optimized for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ +* Perf today: this is hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1734)) for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ * [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): From c13e4b5c75fc9a1b4a025a9a021b843c8821afd8 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 13 Feb 2024 16:44:46 +0100 Subject: [PATCH 11/17] Update equality.md --- docs/equality.md | 75 +++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 3cd9b2cc923..3f559fc11cd 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -9,9 +9,7 @@ This spec is about the semantics and performance of the following coding constru * `a = b` * `a <> b` -It is also about the semantics and performance of uses of the following `FSharp.Core` constructs. - -which, after inlining, generate code that contains an equality check at the specific `EQTYPE` +It is also about the semantics and performance of uses of the following `FSharp.Core` constructs which, after inlining, generate code that contains an equality check at the specific `EQTYPE` * `HashIdentity.Structural<'T>` * `{Array,Seq,List}.contains` * `{Array,Seq,List}.countBy` @@ -51,12 +49,12 @@ Here we define the relevant static type `EQTYPE` for the different constructs ab ### Inlined constructs -* `HashIdentity.Structural<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) -* `Array.contains<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) +* `HashIdentity.Structural<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) +* `Array.contains<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) * `List.contains` likewise * `Seq.contains` likewise -These only resulting in naked generic equality if themselves used from a non-inlined generic context. +These only result in naked generic equality if themselves used from a non-inlined generic context. ### Non-inlined constructs always resulting in naked generic equality @@ -75,7 +73,6 @@ These only resulting in naked generic equality if themselves used from a non-inl * `Seq.distinct` likewise * `Seq.distinctBy` likewise * `Seq.except` likewise -* ... may be more here, see `: equality` constraints in `FSharp.Core` for non-inlined code. These **always** result in naked generic equality checks. @@ -95,7 +92,7 @@ let f2<'T> () = let x = HashIdentity.Structural<'T> ... some long code -f2() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE +f2() // performance of this is determined by EQTYPE<'T> and RUNTIME-EQTYPE ``` Example 3 (an inlined generic context): @@ -108,7 +105,7 @@ let f3<'T> () = let x = HashIdentity.Structural<'T> ... some long code -f3() // performance of this determined by EQTYPE<'T> and RUNTIME-EQTYPE +f3() // performance of this is determined by EQTYPE and RUNTIME-EQTYPE ``` Example 4 (a generic struct type in a non-inline generic context): @@ -133,7 +130,7 @@ Aim here is to flesh these all out with: * **Perf expectation**: what perf the user expects * **Compilation today**: How we actually compile today, with sharplab.io link * **Perf today**: What is the perf we achieve today -* **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure change +* **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure changes * **sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab ### primitive integer types (`int32`, `int64`, ...) @@ -173,42 +170,29 @@ let f (x: float32) (y: float32) = (x = y) * Semantics: User expects structural * Perf: User expects flattening to constituent checks -* Compilation today: tuple equality is flattened to constituent checks up to size 5 ✅ -* Perf today: ok up to size 5 ✅ +* Compilation today: tuple equality is flattened to constituent checks ✅ +* Perf today: good ✅ * [sharplab (int * double * 'T), with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) ### tuple type (size > 5) * Semantics: User expects structural * Perf: User expects flattening to constituent checks -* Compilation today: compiled to GenericEqualityIntrinsic -* Perf today: size > 5 is not flattened. This means the check does type tests, does virtual calls via IStructuralEqualityComparer, boxes etc. ❌(Problem3) +* Compilation today: not flattened, compiled to `GenericEqualityIntrinsic` +* Perf today: the check does type tests, does virtual calls via `IStructuralEqualityComparer`, boxes etc. ❌(Problem1) * [sharplab for size 6](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVI0841MimqyigSidQE9UBeLbL9p+EA==) ### struct tuple type * Semantics: User expects structural -* Perf: User expects flattening to constituent checks, **or at least the same optimizations as tuples** -* Compilation today: compiled to GenericEqualityIntrinsic -* Perf today: boxes, does type tests, does virtual calls via IStructuralEqualityComparer etc. ❌(Problem4) +* Perf: User expects flattening to constituent checks or at least the same optimizations as tuples +* Compilation today: compiled to `GenericEqualityIntrinsic` +* Perf today: boxes, does type tests, does virtual calls via `IStructuralEqualityComparer` etc. ❌(Problem2) * [sharplab for size 3](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lRZAJwFcBjNTASwDs0AqVG+x2gSldQE9UBeLbXl1bwgA=) -### array type (byte[], int[], some-struct-type[], ...) - -* Semantics: User expects structural -* Perf: User expects perf is sum of constituent parts -* Compilation today: GenericEqualityIntrinsic -* Perf today: this is hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1734)) for some primitive element types ✅ but boxes each element if "other" is struct or T, see Problem1, Problem2 ❌ -* [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) - -Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): -* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` -* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized -* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): ❔ - ### C# or F# enum type -* Semantics: User expects identical to equality on underlying +* Semantics: User expects identical to equality on the underlying type * Perf: User expects same perf as flattening to underlying type * Compilation today: flattens to underlying type * Perf today: good ✅ @@ -217,16 +201,16 @@ Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): ### C# struct type -* Semantics: User really expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same +* Semantics: User expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same * Compilation today: `GenericEqualityIntrinsic` -* Perf today: always boxes (❌, Problem1) +* Perf today: always boxes (❌, Problem3) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) ### F# struct type (with compiler-generated structural equality) * Semantics: User expects field-by-field structural equality with no boxing * Compilation today: `GenericEqualityIntrinsic` -* Perf today: always boxes (❌, Problem1b) +* Perf today: always boxes (❌, Problem3b) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA) Note: the optimization path is a bit strange here, the reductions are: @@ -267,13 +251,26 @@ These call each other in sequence, boing then bunboxing then boxing. We do NOT g If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing.] +### array type (byte[], int[], some-struct-type[], ...) + +* Semantics: User expects structural +* Perf: User expects perf is sum of constituent parts +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: this is hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1562)) for some primitive element types ✅ but boxes each element if "other" is struct or generic, see Problem3, Problem4 ❌ +* [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) + +Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): +* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` +* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): ❔ + ### F# large ref record/union type Here "large" means the compiler-generated structural equality is not inlined * Semantics: User expects structural by default * Perf: User expects perf is sum of constituent parts, type-specialized if generic -* Today: direct call to `Equals(T)`, which has specialized code but boxes fields if struct or T, see Problem1, Problem2 ❌ +* Today: direct call to `Equals(T)`, which has specialized code but boxes fields if struct or T, see Problem3, Problem4 ❌ ### F# tiny ref record/union type @@ -283,7 +280,7 @@ Here "tiny" means the compiler-generated structural equality IS inlined * Perf: User expects perf is sum of constituent parts, type-specialized if generic * Test: Equals06.fsx * Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields -* Perf today: boxes on struct and generic fields, see Problem1, Problem2 +* Perf today: boxes on struct and generic fields, see Problem3, Problem4 Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): * [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields @@ -304,7 +301,7 @@ Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): ### Generic `'T` in non-inlined generic code * Semantics: User expects the PER equality semantics of whatever T actually is -* Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) +* Perf: User expects no boxing (❌, Problem4, fails if T is any non-reference type) * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined * Compilation today: GenericEqualityERIntrinsic (❌,boxes) @@ -323,7 +320,7 @@ This case happens in structural equality for tuple types and other structural ty For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) * Semantics: User expects the PER equality semantics of whatever T actually is -* Perf: User expects no boxing (❌, Problem2, fails if T is any non-reference type) +* Perf: User expects no boxing (❌, Problem4, fails if T is any non-reference type) * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` * Perf today: boxes @@ -347,7 +344,7 @@ Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): ### [#5112](https://github.com/dotnet/fsharp/pull/5112) * Uses TT, DEQ, KFS, DV -* Focuses on solving Problem2 +* Focuses on solving Problem4 * Not breaking Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an IEqualityComparer and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. We can simply change the definition in the library like this: From 65ba8c5f503afb3cde979fb5a28b1f63292cd0a9 Mon Sep 17 00:00:00 2001 From: Petr Date: Tue, 13 Feb 2024 16:56:33 +0100 Subject: [PATCH 12/17] Update equality.md --- docs/equality.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 3f559fc11cd..335e07c9069 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -240,16 +240,16 @@ The struct type has these generated methods: ```csharp override bool Equals(object y) override bool Equals(SomeStruct obj) - override bool Equals(object obj, IEqualityComparer comp) //withcEqualsVal + override bool Equals(object obj, IEqualityComparer comp) //with EqualsVal ``` -These call each other in sequence, boing then bunboxing then boxing. We do NOT generate this method, we probably should: +These call each other in sequence, boxing then unboxing then boxing. We do NOT generate this method, we probably should: ```csharp - override bool Equals(SomeStruct obj, IEqualityComparer comp) //withcEqualsValUnboxed + override bool Equals(SomeStruct obj, IEqualityComparer comp) //with EqualsValUnboxed ``` -If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing.] +If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing. ### array type (byte[], int[], some-struct-type[], ...) @@ -266,25 +266,26 @@ Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): ### F# large ref record/union type -Here "large" means the compiler-generated structural equality is not inlined +Here "large" means the compiler-generated structural equality is NOT inlined. * Semantics: User expects structural by default * Perf: User expects perf is sum of constituent parts, type-specialized if generic -* Today: direct call to `Equals(T)`, which has specialized code but boxes fields if struct or T, see Problem3, Problem4 ❌ +* Compilation today: direct call to `Equals(T)` +* Perf today: the call to `Equals(T)` has specialized code but boxes fields if struct or generic, see Problem3, Problem4 ❌ ### F# tiny ref record/union type -Here "tiny" means the compiler-generated structural equality IS inlined +Here "tiny" means the compiler-generated structural equality IS inlined. * Semantics: User expects structural by default * Perf: User expects perf is sum of constituent parts, type-specialized if generic -* Test: Equals06.fsx -* Compilation Today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields -* Perf today: boxes on struct and generic fields, see Problem3, Problem4 +* Compilation today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields +* Perf today: boxes on struct and generic fields, see Problem3, Problem4 ❌ Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): * [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields * NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +* NOTE: The test for this is Equals06.fsx ### Any ref type supporting `IEquatable` @@ -300,10 +301,10 @@ Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): ### Generic `'T` in non-inlined generic code -* Semantics: User expects the PER equality semantics of whatever T actually is -* Perf: User expects no boxing (❌, Problem4, fails if T is any non-reference type) +* Semantics: User expects the PER equality semantics of whatever `'T` actually is +* Perf: User expects no boxing (Problem4 ❌, fails if `'T` is any non-reference type) * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined -* Compilation today: GenericEqualityERIntrinsic (❌,boxes) +* Compilation today: GenericEqualityERIntrinsic (❌, boxes) Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): * Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` From 737d860c49615da267e020a8e461c69eee92afc3 Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 26 Feb 2024 16:46:43 +0100 Subject: [PATCH 13/17] Update equality.md --- docs/equality.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 335e07c9069..f2ffe3c8949 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -1,4 +1,4 @@ -# Compiling Equality and Comparison +# Compiling Equality This spec covers how equality is compiled and executed by the F# compiler and library, based mainly on the types involved in the equality operation after all inlining, type specialization and other optimizations have been applied. @@ -58,7 +58,7 @@ These only result in naked generic equality if themselves used from a non-inline ### Non-inlined constructs always resulting in naked generic equality -* `Array.groupBy<'Key, 'T> f array`, EQTYPE is non-inlined 'Key, results in naked generic equality +* `Array.groupBy<'Key, 'T> f array`, EQTYPE is non-inlined `'Key`, results in naked generic equality * `Array.countBy array` likewise for `'T` * `Array.distinct<'T> array` likewise * `Array.distinctBy array` likewise @@ -128,10 +128,9 @@ This very much depends on the `EQTYPE` involved in the equality as known by the Aim here is to flesh these all out with: * **Semantics**: what semantics the user expects, and what the semantics actually is * **Perf expectation**: what perf the user expects -* **Compilation today**: How we actually compile today, with sharplab.io link +* **Compilation today**: How we actually compile today * **Perf today**: What is the perf we achieve today -* **Test**: An IL baseline test case that pins down how we compile things today and allows us to measure changes -* **sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab +* **Sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab ### primitive integer types (`int32`, `int64`, ...) @@ -166,7 +165,7 @@ let f (x: float32) (y: float32) = (x = y) * [sharplab decimal](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h7LYDG8AtgIbACUxAnuZTQ83gLzEk+eVkwCwAKCA) * [sharplab string](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h4QwBO8AdgOYCUxAnuZTQ8wLzEl68WjALAAoIA==) -### tuple type (size <= 5) +### reference tuple type (size <= 5) * Semantics: User expects structural * Perf: User expects flattening to constituent checks @@ -174,7 +173,7 @@ let f (x: float32) (y: float32) = (x = y) * Perf today: good ✅ * [sharplab (int * double * 'T), with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) -### tuple type (size > 5) +### reference tuple type (size > 5) * Semantics: User expects structural * Perf: User expects flattening to constituent checks @@ -205,8 +204,9 @@ let f (x: float32) (y: float32) = (x = y) * Compilation today: `GenericEqualityIntrinsic` * Perf today: always boxes (❌, Problem3) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) +* Note: ([#5112](https://github.com/dotnet/fsharp/pull/5112) will improve things here since will start avoiding boxing -### F# struct type (with compiler-generated structural equality) +### F# struct type (records, tuples - with compiler-generated structural equality) * Semantics: User expects field-by-field structural equality with no boxing * Compilation today: `GenericEqualityIntrinsic` @@ -264,7 +264,7 @@ Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): * NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized * Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): ❔ -### F# large ref record/union type +### F# large reference record/union type Here "large" means the compiler-generated structural equality is NOT inlined. @@ -273,7 +273,7 @@ Here "large" means the compiler-generated structural equality is NOT inlined. * Compilation today: direct call to `Equals(T)` * Perf today: the call to `Equals(T)` has specialized code but boxes fields if struct or generic, see Problem3, Problem4 ❌ -### F# tiny ref record/union type +### F# tiny reference record/union type Here "tiny" means the compiler-generated structural equality IS inlined. From ea4dc9da4b8d903bb5678e1e412a401507878c3d Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 26 Feb 2024 16:49:10 +0100 Subject: [PATCH 14/17] Update equality.md --- docs/equality.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index f2ffe3c8949..7cc1bf040ec 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -204,7 +204,7 @@ let f (x: float32) (y: float32) = (x = y) * Compilation today: `GenericEqualityIntrinsic` * Perf today: always boxes (❌, Problem3) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) -* Note: ([#5112](https://github.com/dotnet/fsharp/pull/5112) will improve things here since will start avoiding boxing +* Note: ([#16615](https://github.com/dotnet/fsharp/pull/16615) will improve things here since will start avoiding boxing ### F# struct type (records, tuples - with compiler-generated structural equality) @@ -259,10 +259,9 @@ If we did, the devirtualizing optimization should reduce to this directly, which * Perf today: this is hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1562)) for some primitive element types ✅ but boxes each element if "other" is struct or generic, see Problem3, Problem4 ❌ * [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) -Effect of implementing ([#5112](https://github.com/dotnet/fsharp/pull/5112)): -* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` -* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized -* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): ❔ +Effect of implementing ([#16615](https://github.com/dotnet/fsharp/pull/16615)): +* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` +* Perf after [#16615](https://github.com/dotnet/fsharp/pull/16615): ❔ ### F# large reference record/union type @@ -282,9 +281,8 @@ Here "tiny" means the compiler-generated structural equality IS inlined. * Compilation today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields * Perf today: boxes on struct and generic fields, see Problem3, Problem4 ❌ -Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): -* [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields -* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): +* [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields * NOTE: The test for this is Equals06.fsx ### Any ref type supporting `IEquatable` @@ -306,9 +304,8 @@ Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): * Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined * Compilation today: GenericEqualityERIntrinsic (❌, boxes) -Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): -* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` -* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityIntrinsic` calls are internally optimized +Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): +* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` ### Generic `'T` in inlined generic code @@ -325,10 +322,9 @@ For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQC * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` * Perf today: boxes -Effect of [#5112](https://github.com/dotnet/fsharp/pull/5112): -* Compilation after [#5112](https://github.com/dotnet/fsharp/pull/5112): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` -* Perf after [#5112](https://github.com/dotnet/fsharp/pull/5112): TBD, but much better, no boxing in many cases -* NOTE: Proposed adjustment to [#5112](https://github.com/dotnet/fsharp/pull/5112) noted at end of this doc would mean compilation is not changed, and instead `GenericEqualityWithComparerIntrinsic` calls are internally optimized +Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): +* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` +* Perf after [#16615](https://github.com/dotnet/fsharp/pull/16615): TBD, but much better, no boxing in many cases ## Techniques available to us From 220a02d9c0f98ad2d62d3254892439d675905ca0 Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 28 Feb 2024 17:44:18 +0100 Subject: [PATCH 15/17] Update equality.md --- docs/equality.md | 118 +++++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 75 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 7cc1bf040ec..0723fc6a1e0 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -25,12 +25,12 @@ All of which have implied equality checks. Some of these operations are inlined, In math, a (binary) relation is a way to describe a relationship between the elements of sets. "Greater than" is a relation for numbers, "Subset of" is a relation for sets. Here we talk about 3 particular relations: -1) Reflexivity - every element is related to itself -- For integers, `=` is reflexive (`a = a` is always true) and `>` is not (`a > a` is never true) -2) Symmetry - if `a` is related to `b`, then `b` is related to `a` -- For integers, `=` is symmetric (`a = b` -> `b = a`) and `>` is not (if `a > b` then `b > a` is false) -3) Transitivity - if `a` is related to `b`, and `b` is related to `c`, then `a` is also related `c` -- For integers, `>` is transitive (`a > b` && `b > c` -> `a > c`) and `√` is not (`a = √b` && `b = √c` doesn't mean `a = √c`) +1) **Reflexivity** - every element is related to itself + - For integers, `=` is reflexive (`a = a` is always true) and `>` is not (`a > a` is never true) +2) **Symmetry** - if `a` is related to `b`, then `b` is related to `a` + - For integers, `=` is symmetric (`a = b` -> `b = a`) and `>` is not (if `a > b` then `b > a` is false) +3) **Transitivity** - if `a` is related to `b`, and `b` is related to `c`, then `a` is also related `c` + - For integers, `>` is transitive (`a > b` && `b > c` -> `a > c`) and `√` is not (`a = √b` && `b = √c` doesn't mean `a = √c`) If a relation has 1, 2, and 3, we talk about Equivalence Relation (ER). If a relation only has 2 and 3, we talk about Partial Equivalence Relation (PER). @@ -49,8 +49,8 @@ Here we define the relevant static type `EQTYPE` for the different constructs ab ### Inlined constructs -* `HashIdentity.Structural<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) -* `Array.contains<'T>`, EQTYPE is the **inlined** `'T` (results in specialized equality) +* `HashIdentity.Structural<'T>`, `EQTYPE` is the **inlined** `'T` (results in specialized equality) +* `Array.contains<'T>`, `EQTYPE` is the **inlined** `'T` (results in specialized equality) * `List.contains` likewise * `Seq.contains` likewise @@ -58,7 +58,7 @@ These only result in naked generic equality if themselves used from a non-inline ### Non-inlined constructs always resulting in naked generic equality -* `Array.groupBy<'Key, 'T> f array`, EQTYPE is non-inlined `'Key`, results in naked generic equality +* `Array.groupBy<'Key, 'T> f array`, `EQTYPE` is non-inlined `'Key`, results in naked generic equality * `Array.countBy array` likewise for `'T` * `Array.distinct<'T> array` likewise * `Array.distinctBy array` likewise @@ -130,7 +130,8 @@ Aim here is to flesh these all out with: * **Perf expectation**: what perf the user expects * **Compilation today**: How we actually compile today * **Perf today**: What is the perf we achieve today -* **Sharplab**: sharplab.io link to how things are in whatever version is selected in sharplab +* (Optional) sharplab.io link to how things are in whatever version is selected in sharplab +* (Optional) notes ### primitive integer types (`int32`, `int64`, ...) @@ -201,22 +202,26 @@ let f (x: float32) (y: float32) = (x = y) ### C# struct type * Semantics: User expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same +* Perf expected: no boxing * Compilation today: `GenericEqualityIntrinsic` -* Perf today: always boxes (❌, Problem3) +* Perf today: always boxes (Problem3 ❌) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) -* Note: ([#16615](https://github.com/dotnet/fsharp/pull/16615) will improve things here since will start avoiding boxing +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will improve things here since we'll start avoiding boxing ### F# struct type (records, tuples - with compiler-generated structural equality) * Semantics: User expects field-by-field structural equality with no boxing +* Perf expected: no boxing * Compilation today: `GenericEqualityIntrinsic` -* Perf today: always boxes (❌, Problem3b) +* Perf today: always boxes (Problem3 ❌) * [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA) +* Note: the optimization path is a bit strange here, see the reductions below -Note: the optimization path is a bit strange here, the reductions are: +
-```fsharp +Details +```fsharp (x = y) --inline--> @@ -251,80 +256,54 @@ These call each other in sequence, boxing then unboxing then boxing. We do NOT g If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing. +
+ ### array type (byte[], int[], some-struct-type[], ...) * Semantics: User expects structural -* Perf: User expects perf is sum of constituent parts +* Perf expected: User expects perf is sum of constituent parts * Compilation today: `GenericEqualityIntrinsic` -* Perf today: this is hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1562)) for some primitive element types ✅ but boxes each element if "other" is struct or generic, see Problem3, Problem4 ❌ +* Perf today: hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1562)) for some primitive element types ✅ but boxes each element if "other" is struct or generic, see Problem3 ❌, Problem4 ❌ * [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) - -Effect of implementing ([#16615](https://github.com/dotnet/fsharp/pull/16615)): -* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615), either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` -* Perf after [#16615](https://github.com/dotnet/fsharp/pull/16615): ❔ +* Note: ([#16615](https://github.com/dotnet/fsharp/pull/16615)) will improve this compiling to either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` ### F# large reference record/union type Here "large" means the compiler-generated structural equality is NOT inlined. * Semantics: User expects structural by default -* Perf: User expects perf is sum of constituent parts, type-specialized if generic +* Perf expected: User expects perf is sum of constituent parts, type-specialized if generic * Compilation today: direct call to `Equals(T)` -* Perf today: the call to `Equals(T)` has specialized code but boxes fields if struct or generic, see Problem3, Problem4 ❌ +* Perf today: the call to `Equals(T)` has specialized code but boxes fields if struct or generic, see Problem3 ❌, Problem4 ❌ -### F# tiny reference record/union type +### F# tiny reference (anonymous) record or union type Here "tiny" means the compiler-generated structural equality IS inlined. * Semantics: User expects structural by default -* Perf: User expects perf is sum of constituent parts, type-specialized if generic +* Perf expected: User expects perf is sum of constituent parts, type-specialized if generic * Compilation today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields -* Perf today: boxes on struct and generic fields, see Problem3, Problem4 ❌ - -Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): -* [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields -* NOTE: The test for this is Equals06.fsx - -### Any ref type supporting `IEquatable` - -* Semantics: User expects calling `IEquatable` implementation is used, actual is call to `this.Equals(that)`. These are generally identical semantics - -### ref type only supporting .Equals(object) override - -* Semantics: User expects call to this override, perhaps a non-virtual call - -### other ref type - -* Semantics: User expects fast reference equality +* Perf today: boxes on struct and generic fields, see Problem3 ❌, Problem4 ❌ +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will help, compiling to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields ### Generic `'T` in non-inlined generic code * Semantics: User expects the PER equality semantics of whatever `'T` actually is -* Perf: User expects no boxing (Problem4 ❌, fails if `'T` is any non-reference type) -* Test: Equals06.fsx acts as a proxy because equals on small single-case union is inlined -* Compilation today: GenericEqualityERIntrinsic (❌, boxes) - -Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): -* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` - -### Generic `'T` in inlined generic code - -* Semantics: User expects perf same as type specialized code (✅) +* Perf expected: User expects no boxing +* Compilation today: `GenericEqualityERIntrinsic` +* Perf today: boxes if `'T` is any non-reference type (Problem4 ❌) +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will improve this compiling to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` ### Generic `'T` in recursive position in structural comparison This case happens in structural equality for tuple types and other structural types -For example see [this sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) - -* Semantics: User expects the PER equality semantics of whatever T actually is -* Perf: User expects no boxing (❌, Problem4, fails if T is any non-reference type) +* Semantics: User expects the PER equality semantics of whatever `'T` actually is +* Perf: User expects no boxing * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` -* Perf today: boxes - -Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): -* Compilation after [#16615](https://github.com/dotnet/fsharp/pull/16615): ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` -* Perf after [#16615](https://github.com/dotnet/fsharp/pull/16615): TBD, but much better, no boxing in many cases +* Perf today: boxes for if `'T` is any non-reference type - Problem4 ❌ +* [Sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will compile to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` and avoid boxing in many cases ## Techniques available to us @@ -334,7 +313,7 @@ Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): 4. TS: Hand-code type-specializations using static optimization conditions in FSharp.Core 5. TT: Type-indexed tables of baked (poss by reflection) equality comparers and functions, where some pre-computation is done 6. DV: De-virtualization -7. DEQ: Use EqualityComparer<'T>.Default where possible +7. DEQ: Use `EqualityComparer<'T>.Default` where possible ## Notes on previous attempts to improve things @@ -342,17 +321,6 @@ Effect of [#16615](https://github.com/dotnet/fsharp/pull/16615): * Uses TT, DEQ, KFS, DV * Focuses on solving Problem4 -* Not breaking - -Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an IEqualityComparer and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. We can simply change the definition in the library like this: - -```fsharp -let GenericEqualityIntrinsic (x : 'T) (y : 'T) : bool = - FSharpEqualityComparer_PER<'T>.EqualityComparer.Equals(x,y) - -let GenericEqualityERIntrinsic (x : 'T) (y : 'T) : bool = - FSharpEqualityComparer_ER<'T>.EqualityComparer.Equals(x,y) +* 99% not breaking, apart from the case of value types with custom equality implemented differently than the `EqualityComparer.Default` - the change would lead to the usage of the custom implementation which is reasonable -let GenericHashIntrinsic input = - FSharpEqualityComparer_PER<'T>.EqualityComparer.Hash(input) -``` \ No newline at end of file +Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an `IEqualityComparer` and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. \ No newline at end of file From 04f310903a6ee21b9241ec2932a0b850ad4528d5 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 29 Feb 2024 17:36:34 +0100 Subject: [PATCH 16/17] Update equality.md --- docs/equality.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/equality.md b/docs/equality.md index 0723fc6a1e0..acabd8caa2b 100644 --- a/docs/equality.md +++ b/docs/equality.md @@ -32,7 +32,7 @@ Here we talk about 3 particular relations: 3) **Transitivity** - if `a` is related to `b`, and `b` is related to `c`, then `a` is also related `c` - For integers, `>` is transitive (`a > b` && `b > c` -> `a > c`) and `√` is not (`a = √b` && `b = √c` doesn't mean `a = √c`) -If a relation has 1, 2, and 3, we talk about Equivalence Relation (ER). If a relation only has 2 and 3, we talk about Partial Equivalence Relation (PER). +If a relation has 1, 2, and 3, we talk about **Equivalence Relation (ER)**. If a relation only has 2 and 3, we talk about **Partial Equivalence Relation (PER)**. This matters in comparing floats since they include [NaN](https://en.wikipedia.org/wiki/NaN). Depending on if we consider `NaN = NaN` true or false, we talk about ER or PER comparison respectively. @@ -301,7 +301,7 @@ This case happens in structural equality for tuple types and other structural ty * Semantics: User expects the PER equality semantics of whatever `'T` actually is * Perf: User expects no boxing * Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` -* Perf today: boxes for if `'T` is any non-reference type - Problem4 ❌ +* Perf today: boxes for if `'T` is any non-reference type (Problem4 ❌) * [Sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) * Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will compile to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` and avoid boxing in many cases From c9f67e8822e2f6513a3f8dbfc66b9770cd57d191 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 29 Feb 2024 17:38:23 +0100 Subject: [PATCH 17/17] rename --- docs/{equality.md => optimizations-equality.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{equality.md => optimizations-equality.md} (100%) diff --git a/docs/equality.md b/docs/optimizations-equality.md similarity index 100% rename from docs/equality.md rename to docs/optimizations-equality.md