diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e7a71227a63..a3f9bb53187 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -794,7 +794,7 @@ stages: condition: always() # Test trimming on Windows - - job: Build_And_Test_Trimming_Windows + - job: Build_And_Test_AOT_Windows pool: name: $(DncEngPublicBuildPool) demands: ImageOverride -equals $(WindowsMachineQueueName) @@ -825,9 +825,9 @@ stages: env: NativeToolsOnMachine: true displayName: Initial build and prepare packages. - - script: $(Build.SourcesDirectory)/tests/AheadOfTime/Trimming/check.cmd + - script: $(Build.SourcesDirectory)/tests/AheadOfTime/check.cmd displayName: Build, trim, publish and check the state of the trimmed app. - workingDirectory: $(Build.SourcesDirectory)/tests/AheadOfTime/Trimming + workingDirectory: $(Build.SourcesDirectory)/tests/AheadOfTime - task: PublishPipelineArtifact@1 displayName: Publish Trim Tests Logs inputs: diff --git a/docs/release-notes/.FSharp.Core/8.0.300.md b/docs/release-notes/.FSharp.Core/8.0.300.md index a978645b641..7c3911ae98f 100644 --- a/docs/release-notes/.FSharp.Core/8.0.300.md +++ b/docs/release-notes/.FSharp.Core/8.0.300.md @@ -1,6 +1,8 @@ ### Added * Minor tweaks to inline specifications to support Visibility PR ([PR #15484](https://github.com/dotnet/fsharp/pull/15484), [#PR 16427](https://github.com/dotnet/fsharp/pull/15484) +* Optimize equality in generic contexts. ([PR #16615](https://github.com/dotnet/fsharp/pull/16615)) ### Fixed + * Preserve original stack traces in resumable state machines generated code if available. ([PR #16568](https://github.com/dotnet/fsharp/pull/16568)) diff --git a/eng/Build.ps1 b/eng/Build.ps1 index 5bf5d00bdc5..bb449e05665 100644 --- a/eng/Build.ps1 +++ b/eng/Build.ps1 @@ -680,7 +680,7 @@ try { } if ($testAOT) { - Push-Location "$RepoRoot\tests\AheadOfTime\Trimming" + Push-Location "$RepoRoot\tests\AheadOfTime" ./check.cmd Pop-Location } diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index a6d5bcc6ac0..346c1a64e0e 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -1549,8 +1549,8 @@ namespace Microsoft.FSharp.Core // Run in either PER or ER mode. In PER mode, equality involving a NaN returns "false". // In ER mode, equality on two NaNs returns "true". // - // If "er" is true the "iec" is fsEqualityComparerNoHashingER - // If "er" is false the "iec" is fsEqualityComparerNoHashingPER + // If "er" is true the "iec" is fsEqualityComparerUnlimitedHashingER + // If "er" is false the "iec" is fsEqualityComparerUnlimitedHashingPER let rec GenericEqualityObj (er:bool) (iec:IEqualityComparer) ((xobj:obj),(yobj:obj)) : bool = (*if objEq xobj yobj then true else *) match xobj,yobj with @@ -1654,28 +1654,186 @@ namespace Microsoft.FSharp.Core else i <- i + 1 res + let isStructuralEquatable (ty: Type) = typeof.IsAssignableFrom ty + let isArray (ty: Type) = ty.IsArray || (typeof.IsAssignableFrom ty) + let isFloat (ty: Type) = Type.op_Equality(ty, typeof) || Type.op_Equality(ty, typeof) + + let isValueTuple (ty: Type) = + if ty.IsGenericType + then + let typeDef = ty.GetGenericTypeDefinition().FullName + typeDef.Equals "System.ValueTuple`1" || + typeDef.Equals "System.ValueTuple`2" || + typeDef.Equals "System.ValueTuple`3" || + typeDef.Equals "System.ValueTuple`4" || + typeDef.Equals "System.ValueTuple`5" || + typeDef.Equals "System.ValueTuple`6" || + typeDef.Equals "System.ValueTuple`7" || + typeDef.Equals "System.ValueTuple`8" + else false + + let isOptionOrResult (ty: Type) = + if ty.IsGenericType + then + let typeDef = ty.GetGenericTypeDefinition().FullName + typeDef.Equals "Microsoft.FSharp.Core.FSharpOption`1" || + typeDef.Equals "Microsoft.FSharp.Core.FSharpValueOption`1" || + typeDef.Equals "Microsoft.FSharp.Core.FSharpResult`2" + else false + + let isNullable (ty: Type) = + ty.IsGenericType && + ty.GetGenericTypeDefinition().FullName.Equals "System.Nullable`1" + + // A bit hard to grasp right away but think of this as if it was: + // + // let canUseDefaultEqualityComparer rootType = + // let processed = HashSet() + // + // let rec checkTypes types = + // match types with + // | [] -> true + // | ty :: rest -> + // if (processed.Add ty) then + // ... do all the checks + // checkTypes rest + // else + // checkTypes rest + // + // checkTypes [ rootType ] + // + // ... it's just that here in prim types, not much stuff is available yet + // so we have to resort to very basic techniques + let canUseDefaultEqualityComparer er rootType = + let processed = HashSet() - /// One of the two unique instances of System.Collections.IEqualityComparer. Implements PER semantics - /// where equality on NaN returns "false". - let fsEqualityComparerNoHashingPER = - { new IEqualityComparer with - override iec.Equals(x:obj,y:obj) = GenericEqualityObj false iec (x,y) // PER Semantics - override iec.GetHashCode(x:obj) = raise (InvalidOperationException (SR.GetString(SR.notUsedForHashing))) } - - /// One of the two unique instances of IEqualityComparer. Implements ER semantics - /// where equality on NaN returns "true". - let fsEqualityComparerNoHashingER = - { new IEqualityComparer with - override iec.Equals(x:obj,y:obj) = GenericEqualityObj true iec (x,y) // ER Semantics - override iec.GetHashCode(x:obj) = raise (InvalidOperationException (SR.GetString(SR.notUsedForHashing))) } + let rec checkType index (types: Type array) = + if types.Length = index then true + else + let ty = get types index + + if processed.Add ty + then + // avoid any types that need special handling in GenericEqualityObj + // GenericEqualityObj handles string as a special case, but internally routes to same equality + + // covers enum and value types + // reference types need to be sealed as derived class might implement IStructuralEquatable + ty.IsSealed + + // handled elsewhere in a specialized fashion + && not (isArray ty) + && (er || not (isFloat ty)) + + // analyze generic types + && (match ty with + | ty when isNullable ty || + isStructuralEquatable ty && isValueTuple ty || + isStructuralEquatable ty && isOptionOrResult ty -> checkType 0 (ty.GetGenericArguments()) + + | ty when isStructuralEquatable ty -> false + + | _ -> true) + + && checkType (index + 1) types + + else + checkType (index + 1) types + + checkType 0 [| rootType |] + + let arrayEqualityComparer<'T> er comparer = + let arrayEquals (er: bool) (iec: IEqualityComparer) (xobj: obj) (yobj: obj) : bool = + match xobj, yobj with + | null, null -> true + | null, _ -> false + | _, null -> false + | (:? (obj array) as arr1), (:? (obj array) as arr2) -> GenericEqualityObjArray er iec arr1 arr2 + | (:? (byte array) as arr1), (:? (byte array) as arr2) -> GenericEqualityByteArray arr1 arr2 + | (:? (int32 array) as arr1), (:? (int32 array) as arr2) -> GenericEqualityInt32Array arr1 arr2 + | (:? (int64 array) as arr1), (:? (int64 array) as arr2) -> GenericEqualityInt64Array arr1 arr2 + | (:? (char array) as arr1), (:? (char array) as arr2) -> GenericEqualityCharArray arr1 arr2 + | (:? (float32 array) as arr1), (:? (float32 array) as arr2) -> GenericEqualitySingleArray er arr1 arr2 + | (:? (float array) as arr1), (:? (float array) as arr2) -> GenericEqualityDoubleArray er arr1 arr2 + | (:? Array as arr1), (:? Array as arr2) -> GenericEqualityArbArray er iec arr1 arr2 + | _ -> raise (Exception "invalid logic - expected array") + + let getHashCode (iec, xobj: obj) = + match xobj with + | null -> 0 + | :? (obj array) as oa -> GenericHashObjArray iec oa + | :? (byte array) as ba -> GenericHashByteArray ba + | :? (int array) as ia -> GenericHashInt32Array ia + | :? (int64 array) as ia -> GenericHashInt64Array ia + | :? Array as a -> GenericHashArbArray iec a + | _ -> raise (Exception "invalid logic - expected array") + + { new EqualityComparer<'T>() with + member _.Equals (x, y) = arrayEquals er comparer (box x) (box y) + member _.GetHashCode x = getHashCode (fsEqualityComparerUnlimitedHashingPER, box x) } + + let structuralEqualityComparer<'T> comparer = + { new EqualityComparer<'T>() with + member _.Equals (x,y) = + match box x, box y with + | null, null -> true + | null, _ -> false + | _, null -> false + | (:? IStructuralEquatable as x1), yobj -> x1.Equals (yobj, comparer) + | _ -> raise (Exception "invalid logic - expected IStructuralEquatable") + + member _.GetHashCode x = + match box x with + | null -> 0 + | :? IStructuralEquatable as a -> a.GetHashCode fsEqualityComparerUnlimitedHashingPER + | _ -> raise (Exception "invalid logic - expected IStructuralEquatable") } + + let getEqualityComparer<'T> er = + let ty = typeof<'T> + + match er with + | _ when canUseDefaultEqualityComparer er ty -> EqualityComparer<'T>.Default + + | true when isArray ty -> arrayEqualityComparer true fsEqualityComparerUnlimitedHashingER + | false when isArray ty -> arrayEqualityComparer false fsEqualityComparerUnlimitedHashingPER + | true when isStructuralEquatable ty -> structuralEqualityComparer fsEqualityComparerUnlimitedHashingER + | false when isStructuralEquatable ty -> structuralEqualityComparer fsEqualityComparerUnlimitedHashingPER + + | false when Type.op_Equality(ty, typeof) -> unboxPrim (box { + new EqualityComparer() with + member _.Equals (x,y) = (# "ceq" x y : bool #) + member _.GetHashCode x = x.GetHashCode () }) + + | false when Type.op_Equality(ty, typeof) -> unboxPrim (box { + new EqualityComparer() with + member _.Equals (x,y) = (# "ceq" x y : bool #) + member _.GetHashCode x = x.GetHashCode () }) + + | false -> { + new EqualityComparer<'T>() with + member _.Equals (x,y) = GenericEqualityObj false fsEqualityComparerUnlimitedHashingPER (box x, box y) + member _.GetHashCode x = GenericHashParamObj fsEqualityComparerUnlimitedHashingPER (box x) } + + | true -> { + new EqualityComparer<'T>() with + member _.Equals (x,y) = GenericEqualityObj true fsEqualityComparerUnlimitedHashingER (box x, box y) + member _.GetHashCode x = GenericHashParamObj fsEqualityComparerUnlimitedHashingER (box x) } + + type EqualityComparerER<'T> () = + + static member val Comparer = getEqualityComparer<'T> true + + type EqualityComparerPER<'T> () = + + static member val Comparer = getEqualityComparer<'T> false /// Implements generic equality between two values, with PER semantics for NaN (so equality on two NaN values returns false) // // The compiler optimizer is aware of this function (see use of generic_equality_per_inner_vref in opt.fs) // and devirtualizes calls to it based on "T". let GenericEqualityIntrinsic (x : 'T) (y : 'T) : bool = - GenericEqualityObj false fsEqualityComparerNoHashingPER ((box x), (box y)) - + EqualityComparerPER<'T>.Comparer.Equals (x, y) + /// Implements generic equality between two values, with ER semantics for NaN (so equality on two NaN values returns true) // // ER semantics is used for recursive calls when implementing .Equals(that) for structural data, see the code generated for record and union types in augment.fs @@ -1683,15 +1841,20 @@ namespace Microsoft.FSharp.Core // The compiler optimizer is aware of this function (see use of generic_equality_er_inner_vref in opt.fs) // and devirtualizes calls to it based on "T". let GenericEqualityERIntrinsic (x : 'T) (y : 'T) : bool = - GenericEqualityObj true fsEqualityComparerNoHashingER ((box x), (box y)) + EqualityComparerER<'T>.Comparer.Equals (x, y) /// Implements generic equality between two values using "comp" for recursive calls. // // The compiler optimizer is aware of this function (see use of generic_equality_withc_inner_vref in opt.fs) // and devirtualizes calls to it based on "T", and under the assumption that "comp" - // is either fsEqualityComparerNoHashingER or fsEqualityComparerNoHashingPER. + // is either fsEqualityComparerUnlimitedHashingER or fsEqualityComparerUnlimitedHashingPER. let GenericEqualityWithComparerIntrinsic (comp : IEqualityComparer) (x : 'T) (y : 'T) : bool = - comp.Equals((box x),(box y)) + if obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingPER) then + EqualityComparerPER<'T>.Comparer.Equals (x, y) + elif obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingER) then + EqualityComparerER<'T>.Comparer.Equals (x, y) + else + comp.Equals ((box x), (box y)) /// Implements generic equality between two values, with ER semantics for NaN (so equality on two NaN values returns true) @@ -1810,7 +1973,7 @@ namespace Microsoft.FSharp.Core // // NOTE: The compiler optimizer is aware of this function (see uses of generic_hash_inner_vref in opt.fs) // and devirtualizes calls to it based on type "T". - let GenericHashIntrinsic input = GenericHashParamObj fsEqualityComparerUnlimitedHashingPER (box input) + let GenericHashIntrinsic input = EqualityComparerPER<'T>.Comparer.GetHashCode input /// Intrinsic for calls to depth-limited structural hashing that were not optimized by static conditionals. let LimitedGenericHashIntrinsic limit input = GenericHashParamObj (CountLimitedHasherPER(limit)) (box input) @@ -1823,7 +1986,10 @@ namespace Microsoft.FSharp.Core // NOTE: The compiler optimizer is aware of this function (see uses of generic_hash_withc_inner_vref in opt.fs) // and devirtualizes calls to it based on type "T". let GenericHashWithComparerIntrinsic<'T> (comp : IEqualityComparer) (input : 'T) : int = - GenericHashParamObj comp (box input) + if obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingPER) then + EqualityComparerPER<'T>.Comparer.GetHashCode input + else + GenericHashParamObj comp (box input) let inline HashString (s:string) = match s with @@ -2136,46 +2302,8 @@ namespace Microsoft.FSharp.Core member _.GetHashCode(x) = GenericLimitedHash limit x member _.Equals(x,y) = GenericEquality x y } - let BoolIEquality = MakeGenericEqualityComparer() - let CharIEquality = MakeGenericEqualityComparer() - let StringIEquality = MakeGenericEqualityComparer() - let SByteIEquality = MakeGenericEqualityComparer() - let Int16IEquality = MakeGenericEqualityComparer() - let Int32IEquality = MakeGenericEqualityComparer() - let Int64IEquality = MakeGenericEqualityComparer() - let IntPtrIEquality = MakeGenericEqualityComparer() - let ByteIEquality = MakeGenericEqualityComparer() - let UInt16IEquality = MakeGenericEqualityComparer() - let UInt32IEquality = MakeGenericEqualityComparer() - let UInt64IEquality = MakeGenericEqualityComparer() - let UIntPtrIEquality = MakeGenericEqualityComparer() - let FloatIEquality = MakeGenericEqualityComparer() - let Float32IEquality = MakeGenericEqualityComparer() - let DecimalIEquality = MakeGenericEqualityComparer() - - type FastGenericEqualityComparerTable<'T>() = - static let f : IEqualityComparer<'T> = - match typeof<'T> with - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box BoolIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box ByteIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box CharIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box SByteIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int16IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int64IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box IntPtrIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt16IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt64IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UIntPtrIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box FloatIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Float32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box DecimalIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box StringIEquality) - | _ -> MakeGenericEqualityComparer<'T>() - static member Function : IEqualityComparer<'T> = f - - let FastGenericEqualityComparerFromTable<'T> = FastGenericEqualityComparerTable<'T>.Function + let FastGenericEqualityComparerFromTable<'T> = + HashCompare.EqualityComparerPER<'T>.Comparer : IEqualityComparer<'T> // This is the implementation of HashIdentity.Structural. In most cases this just becomes // FastGenericEqualityComparerFromTable. diff --git a/tests/AheadOfTime/Equality/Equality.fsproj b/tests/AheadOfTime/Equality/Equality.fsproj new file mode 100644 index 00000000000..f5a243c65b9 --- /dev/null +++ b/tests/AheadOfTime/Equality/Equality.fsproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + preview + true + + + + true + true + true + true + win-x64 + + + + $(MSBuildThisFileDirectory)../../../artifacts/bin/fsc/Release/net8.0/fsc.dll + $(MSBuildThisFileDirectory)../../../artifacts/bin/fsc/Release/net8.0/fsc.dll + False + True + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/AheadOfTime/Equality/Program.fs b/tests/AheadOfTime/Equality/Program.fs new file mode 100644 index 00000000000..052d2875da3 --- /dev/null +++ b/tests/AheadOfTime/Equality/Program.fs @@ -0,0 +1,77 @@ +open System.Collections.Generic +open NonStructuralComparison + +let failures = HashSet() + +let reportFailure (s: string) = + stderr.Write " NO: " + stderr.WriteLine s + failures.Add s |> ignore + +let test testName x y = + let result = HashIdentity.Structural.Equals(x, y) + if result = false + then + stderr.WriteLine($"\n***** {testName}: Expected: 'true' Result: '{result}' = FAIL\n"); + reportFailure testName + +module BasicTypes = + test "test000" true true + test "test001" 1y 1y + test "test002" 1uy 1uy + test "test003" 1s 1s + test "test004" 1us 1us + test "test005" 1 1 + test "test006" 1u 1u + test "test007" 1L 1L + test "test008" 1UL 1UL + test "test009" (nativeint 1) (nativeint 1) + test "test010" (unativeint 1) (unativeint 1) + test "test011" 'a' 'a' + test "test012" "a" "a" + test "test013" 1m 1m + test "test014" 1.0 1.0 + test "test015" 1.0f 1.0f + +module Arrays = + test "test100" [|1|] [|1|] + test "test101" [|1L|] [|1L|] + test "test102" [|1uy|] [|1uy|] + test "test103" [|box 1|] [|box 1|] + +module Structs = + test "test200" struct (1, 1) struct (1, 1) + test "test201" struct (1, 1, 1) struct (1, 1, 1) + test "test202" struct (1, 1, 1, 1) struct (1, 1, 1, 1) + test "test203" struct (1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1) + test "test204" struct (1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1) + test "test205" struct (1, 1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1, 1) + test "test206" struct (1, 1, 1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1, 1, 1) + +module OptionsAndCo = + open System + + test "test301" (Some 1) (Some 1) + test "test302" (ValueSome 1) (ValueSome 1) + test "test303" (Ok 1) (Ok 1) + test "test304" (Nullable 1) (Nullable 1) + +module Enums = + open System + + type SomeEnum = + | Case0 = 0 + | Case1 = 1 + + test "test401" (enum(1)) (enum(1)) + test "test402" (enum(1)) (enum(1)) + +[] +let main _ = + match failures with + | set when set.Count = 0 -> + stdout.WriteLine "All tests passed" + exit 0 + | _ -> + stdout.WriteLine "Some tests failed" + exit 1 diff --git a/tests/AheadOfTime/Equality/check.ps1 b/tests/AheadOfTime/Equality/check.ps1 new file mode 100644 index 00000000000..7913d24599c --- /dev/null +++ b/tests/AheadOfTime/Equality/check.ps1 @@ -0,0 +1,32 @@ +Write-Host "Publish and Execute: net8.0 - Equality" + +$build_output = dotnet publish -restore -c release -f:net8.0 $(Join-Path $PSScriptRoot Equality.fsproj) + +# Checking that the build succeeded +if ($LASTEXITCODE -ne 0) +{ + Write-Error "Build failed with exit code ${LASTEXITCODE}" + Write-Error "${build_output}" -ErrorAction Stop +} + +$process = Start-Process ` + -FilePath $(Join-Path $PSScriptRoot bin\release\net8.0\win-x64\publish\Equality.exe) ` + -Wait ` + -NoNewWindow ` + -PassThru ` + -RedirectStandardOutput $(Join-Path $PSScriptRoot output.txt) + +$output = Get-Content $(Join-Path $PSScriptRoot output.txt) + +# Checking that it is actually running. +if ($LASTEXITCODE -ne 0) +{ + Write-Error "Test failed with exit code ${LASTEXITCODE}" -ErrorAction Stop +} + +# Checking that the output is as expected. +$expected = "All tests passed" +if ($output -ne $expected) +{ + Write-Error "Test failed with unexpected output:`nExpected:`n`t${expected}`nActual`n`t${output}" -ErrorAction Stop +} diff --git a/tests/AheadOfTime/Trimming/check.cmd b/tests/AheadOfTime/Trimming/check.cmd deleted file mode 100644 index 4eefff011c5..00000000000 --- a/tests/AheadOfTime/Trimming/check.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0check.ps1"""" diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index 1dcdf28f8af..7412b21232c 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -39,7 +39,7 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) { # error NETSDK1124: Trimming assemblies requires .NET Core 3.0 or higher. # Check net7.0 trimmed assemblies -CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 287232 +CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 284160 # Check net8.0 trimmed assemblies -CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8820736 +CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8817152 diff --git a/tests/AheadOfTime/check.cmd b/tests/AheadOfTime/check.cmd new file mode 100644 index 00000000000..a52368e2c6f --- /dev/null +++ b/tests/AheadOfTime/check.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Trimming\check.ps1"""" +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Equality\check.ps1"""" diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs index a081437e1f0..cce09739135 100644 --- a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs @@ -1,11 +1,11 @@ -namespace TaskPerf +namespace MicroPerf open BenchmarkDotNet.Running -module Main = +module Main = [] - let main _ = + let main args = printfn "Running benchmarks..." - let _ = BenchmarkRunner.Run() - 0 + BenchmarkSwitcher.FromAssembly(typeof.Assembly).Run(args) |> ignore + 0 diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs new file mode 100644 index 00000000000..742385d7720 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs @@ -0,0 +1,23 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +type Arrays() = + + let numbers = Array.init 1000 id + + [] + member _.Int32() = + numbers |> Array.countBy (fun n -> [| n % 7 |]) + + [] + member _.Int64() = + numbers |> Array.countBy (fun n -> [| int64 (n % 7) |]) + + [] + member _.Byte() = + numbers |> Array.countBy (fun n -> [| byte (n % 7) |]) + + [] + member _.Obj() = + numbers |> Array.countBy (fun n -> [| box (n % 7) |]) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs new file mode 100644 index 00000000000..219284fe732 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs @@ -0,0 +1,63 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type BasicTypes() = + + let bools = Array.init 1000 (fun n -> n % 2 = 0) + let sbytes = Array.init 1000 sbyte + let bytes = Array.init 1000 byte + let int16s = Array.init 1000 int16 + let uint16s = Array.init 1000 uint16 + let int32s = Array.init 1000 id + let uint32s = Array.init 1000 uint32 + let int64s = Array.init 1000 int64 + let uint64s = Array.init 1000 uint64 + let intptrs = Array.init 1000 nativeint + let uintptrs = Array.init 1000 unativeint + let chars = Array.init 1000 char + let strings = Array.init 1000 string + let decimals = Array.init 1000 decimal + + [] + member _.Bool() = bools |> Array.distinct + + [] + member _.SByte() = sbytes |> Array.distinct + + [] + member _.Byte() = bytes |> Array.distinct + + [] + member _.Int16() = int16s |> Array.distinct + + [] + member _.UInt16() = uint16s |> Array.distinct + + [] + member _.Int32() = int32s |> Array.distinct + + [] + member _.UInt32() = uint32s |> Array.distinct + + [] + member _.Int64() = int64s |> Array.distinct + + [] + member _.UInt64() = uint64s |> Array.distinct + + [] + member _.IntPtr() = intptrs |> Array.distinct + + [] + member _.UIntPtr() = uintptrs |> Array.distinct + + [] + member _.Char() = chars |> Array.distinct + + [] + member _.String() = strings |> Array.distinct + + [] + member _.Decimal() = decimals |> Array.distinct diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs new file mode 100644 index 00000000000..7b26d287ab9 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs @@ -0,0 +1,102 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type SomeStruct = + val A : int + new a = { A = a } + +type FSharpCoreFunctions() = + + let array = Array.init 1000 id + let list = List.init 1000 id + let seq = Seq.init 1000 id + + [] + member _.ArrayCountBy() = + array + |> Array.countBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayGroupBy() = + array + |> Array.groupBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayDistinct() = + array + |> Array.map (fun n -> SomeStruct(n % 7)) + |> Array.distinct + + [] + member _.ArrayDistinctBy() = + array + |> Array.distinctBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayExcept() = + array + |> Array.map SomeStruct + |> Array.except ([| SomeStruct 42 |]) + + [] + member _.ListCountBy() = + list + |> List.countBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListGroupBy() = + list + |> List.groupBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListDistinct() = + list + |> List.map (fun n -> SomeStruct(n % 7)) + |> List.distinct + + [] + member _.ListDistinctBy() = + list + |> List.distinctBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListExcept() = + List.init 1000 id + |> List.map SomeStruct + |> List.except ([| SomeStruct 42 |]) + + [] + member _.SeqCountBy() = + seq + |> Seq.countBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqGroupBy() = + seq + |> Seq.groupBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqDistinct() = + seq + |> Seq.map (fun n -> SomeStruct(n % 7)) + |> Seq.distinct + |> Seq.last + + [] + member _.SeqDistinctBy() = + seq + |> Seq.distinctBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqExcept() = + seq + |> Seq.map SomeStruct + |> Seq.except ([| SomeStruct 42 |]) + |> Seq.last + + diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs new file mode 100644 index 00000000000..22234e0da5e --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs @@ -0,0 +1,20 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type Floats() = + + let numbers = Array.init 1000 (fun id -> id % 7) + + [] + member _.FloatER() = numbers |> Array.groupBy float + + [] + member _.Float32ER() = numbers |> Array.groupBy float32 + + [] + member _.FloatPER() = numbers |> Array.Parallel.groupBy float + + [] + member _.Float32PER() = numbers |> Array.Parallel.groupBy float32 \ No newline at end of file diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs new file mode 100644 index 00000000000..fa7c445c620 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs @@ -0,0 +1,78 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +open System + +[] +type BigStruct = + val A : int64 + val B : int64 + new (a, b) = { A = a; B = b } + +[] +type Container<'a> = + val Item : 'a + new i = { Item = i } + +type RandomRecord = { + Field1 : int + Field2 : string + Field3 : byte +} + +[] +type RandomRecordStruct = { + Field1S : int + Field2S : string + Field3S : byte +} + +type RandomGeneric<'a> = RandomGeneric of 'a * 'a + +[] +type Misc() = + + let createBigStruct() = + let n = Random().NextInt64() + Container (BigStruct (n, n)) + + [] + member _.BigStruct() = + let set = Set.empty + for _ = 0 to 200000 do + set.Add (createBigStruct ()) |> ignore + + [] + member _.Record() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> { + Field1 = n + Field2 = string n + Field3 = byte n + }) + + [] + member _.RecordStruct() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> { + Field1S = n + Field2S = string n + Field3S = byte n + }) + + // BDN can't work with F# anon records yet + // https://github.com/dotnet/BenchmarkDotNet/issues/2530 + // [] + member _.AnonymousRecord() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> {| + Field1A = n + Field2A = string n + Field3A = byte n + |}) + + [] + member _.GenericUnion() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> RandomGeneric(n, n)) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs new file mode 100644 index 00000000000..a31f53e3367 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs @@ -0,0 +1,45 @@ +namespace Equality + +open System +open BenchmarkDotNet.Attributes + +[] +type OptionsAndCo() = + + let numbers = Array.init 1000 id + + let createOption x = + match x with + | x when x % 2 = 0 -> Some x + | _ -> None + + let createValueOption x = + match x with + | x when x % 2 = 0 -> ValueSome x + | _ -> ValueNone + + let createResult x = + match x with + | x when x % 2 = 0 -> Ok x + | x -> Error x + + let createNullable x = + match x with + | x when x % 2 = 0 -> Nullable x + | _ -> Nullable 42 + + [] + member _.Option() = + numbers |> Array.countBy createOption + + [] + member _.ValueOption() = + numbers |> Array.countBy createValueOption + + [] + member _.Result() = + numbers |> Array.countBy createResult + + [] + member _.Nullable() = + numbers |> Array.countBy createNullable diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs new file mode 100644 index 00000000000..51f6f1689aa --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs @@ -0,0 +1,39 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type Structs() = + + let numbers = Array.init 1000 id + + let createStruct3 (x: int) = struct (x, x, x) + let createStruct4 (x: int) = struct (x, x, x, x) + let createStruct5 (x: int) = struct (x, x, x, x, x) + let createStruct6 (x: int) = struct (x, x, x, x, x, x) + let createStruct7 (x: int) = struct (x, x, x, x, x, x, x) + let createStruct8 (x: int) = struct (x, x, x, x, x, x, x, x) + + [] + member _.Struct3() = + numbers |> Array.countBy (fun n -> createStruct3 n) + + [] + member _.Struct4() = + numbers |> Array.countBy (fun n -> createStruct4 n) + + [] + member _.Struct5() = + numbers |> Array.countBy (fun n -> createStruct5 n) + + [] + member _.Struct6() = + numbers |> Array.countBy (fun n -> createStruct6 n) + + [] + member _.Struct7() = + numbers |> Array.countBy (fun n -> createStruct7 n) + + [] + member _.Struct8() = + numbers |> Array.countBy (fun n -> createStruct8 n) \ No newline at end of file diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs new file mode 100644 index 00000000000..49b0dcd428c --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs @@ -0,0 +1,67 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +type SmallNonGenericTuple = SmallNonGenericTuple of int * string + +type SmallGenericTuple<'a> = SmallGenericTuple of int * 'a + +type BigNonGenericTuple = BigNonGenericTuple of int * string * byte * int * string * byte + +type BigGenericTuple<'a> = BigGenericTuple of int * 'a * byte * int * 'a * byte + +[] +type SmallNonGenericTupleStruct = SmallNonGenericTupleStruct of int * string + +[] +type SmallGenericTupleStruct<'a> = SmallGenericTupleStruct of int * 'a + +[] +type BigNonGenericTupleStruct = BigNonGenericTupleStruct of int * string * byte * int * string * byte + +[] +type BigGenericTupleStruct<'a> = BigGenericTupleStruct of int * 'a * byte * int * 'a * byte + +type ReferenceTuples() = + + let numbers = Array.init 1000 id + + [] + member _.SmallNonGenericTuple() = + numbers + |> Array.countBy (fun n -> SmallNonGenericTuple(n, string n)) + + [] + member _.SmallGenericTuple() = + numbers + |> Array.countBy (fun n -> SmallGenericTuple(n, string n)) + + [] + member _.BigNonGenericTuple() = + numbers + |> Array.countBy (fun n -> BigNonGenericTuple(n, string n, byte n, n, string n, byte n)) + + [] + member _.BigGenericTuple() = + numbers + |> Array.countBy (fun n -> BigGenericTuple(n, string n, byte n, n, string n, byte n)) + + [] + member _.SmallNonGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> SmallNonGenericTupleStruct(n, string n)) + + [] + member _.SmallGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> SmallGenericTupleStruct(n, string n)) + + [] + member _.BigNonGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> BigNonGenericTupleStruct(n, string n, byte n, n, string n, byte n)) + + [] + member _.BigGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> BigGenericTupleStruct(n, string n, byte n, n, string n, byte n)) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs new file mode 100644 index 00000000000..5e48a1c6a91 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs @@ -0,0 +1,39 @@ +namespace Equality + +open System +open BenchmarkDotNet.Attributes + +type SomeEnum = + | Case0 = 0 + | Case1 = 1 + | Case2 = 2 + +[] +type ValueTypes() = + + let numbers = Array.init 1000 id + let now = DateTimeOffset.Now + + let createFSharpStruct (x: int) = + struct (x % 7, x % 7) + + let createFSharpEnum (x: int) = + enum(x % 3) + + let createCSharpStruct (x: int) = + now.AddMinutes x + + let createCSharpEnum (x: int) = + enum(x % 7) + + [] + member _.FSharpStruct() = numbers |> Array.countBy createFSharpStruct + + [] + member _.FSharpEnum() = numbers |> Array.countBy createFSharpEnum + + [] + member _.CSharpStruct() = numbers |> Array.countBy createCSharpStruct + + [] + member _.CSharpEnum() = numbers |> Array.countBy createCSharpEnum diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj index c96d342298f..fc36640d7e6 100644 --- a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj @@ -1,4 +1,4 @@ - + $(FSharpNetCoreProductTargetFramework) Exe @@ -11,6 +11,15 @@ $(OtherFlags) --define:PREVIEW + + + + + + + + +