From 16a078d442488500d5d0b877b68ca8a418beb9fb Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:23:54 -0700 Subject: [PATCH 1/7] Optimize array slicing Array slicing get and set delegated to System.Array.Copy. --- src/FSharp.Core/prim-types.fs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index 0fb2b87a06d..69d35ae603a 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -779,17 +779,17 @@ namespace Microsoft.FSharp.Core let inline SetArray (target: 'T array) (index:int) (value:'T) = (# "stelem.any !0" type ('T) target index value #) - let inline GetArraySub arr (start:int) (len:int) = - let len = if len < 0 then 0 else len - let dst = zeroCreate len - for i = 0 to len - 1 do - SetArray dst i (GetArray arr (start + i)) - dst - - let inline SetArraySub arr (start:int) (len:int) (src:_ array) = - for i = 0 to len - 1 do - SetArray arr (start+i) (GetArray src i) + let inline GetArraySub (arr: 'a array) (start:int) (len:int) : 'a array = + if len <= 0 then + zeroCreate 0 + else + let dst = zeroCreate len + Array.Copy(arr, start, dst, 0, len) + dst + let inline SetArraySub (arr: 'T array) (start:int) (len:int) (src: 'T array) = + if len > 0 then + Array.Copy(src, 0, arr, start, len) let inline GetArray2D (source: 'T[,]) (index1: int) (index2: int) = (# "ldelem.multi 2 !0" type ('T) source index1 index2 : 'T #) From 3218c643a8e4dd0263a88a8382a647869ce28764 Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:49:46 -0700 Subject: [PATCH 2/7] Array slicing benchmark and unit test --- .../Microsoft.FSharp.Collections/ArrayModule.fs | 11 +++++++++++ .../MicroPerf/ArraySlicing.fs | 15 +++++++++++++++ .../MicroPerf/MicroPerf.fsproj | 1 + 3 files changed, 27 insertions(+) create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/ArraySlicing.fs diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs index 16bc5d4fbdf..302f44e85ec 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs @@ -1962,6 +1962,17 @@ type ArrayModule() = Assert.AreEqual(arr.[-4..(-3)], ([||]: int array)) + [] + member _.``Slicing always creates new arrays`` () = + let arr = [|1;2;3;4;5;6|] + + Assert.False(Object.ReferenceEquals(arr[1..3], arr[1..3])) + Assert.False(Object.ReferenceEquals(arr[0..], arr[0..])) + Assert.False(Object.ReferenceEquals(arr[..^1], arr[..^1])) + + Assert.AreEqual(arr[1..-1].Length, 0) + Assert.False(Object.ReferenceEquals(arr[1..-1], arr[1..-1])) + [] member _.RandomShuffle() = let arr = [| 1..20 |] diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/ArraySlicing.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/ArraySlicing.fs new file mode 100644 index 00000000000..5d08be22c52 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/ArraySlicing.fs @@ -0,0 +1,15 @@ +module ArraySlicing + +open BenchmarkDotNet.Attributes +open System + +[] +[] +[] +[] +type ArraySlicingBenchmark() = + let b = [| for _ in 1 .. 100_000 -> byte DateTime.Now.Ticks |] + + [] + member _.x () = + b[ 20 .. 4999 ] \ No newline at end of file diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj index 2830bcffe41..72a9aa08670 100644 --- a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj @@ -26,6 +26,7 @@ + From 7c25e377dc9b10a9087572650dad7b5033930341 Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:31:11 -0700 Subject: [PATCH 3/7] Update release note --- docs/release-notes/.FSharp.Core/10.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Core/10.0.100.md b/docs/release-notes/.FSharp.Core/10.0.100.md index 10ff967c125..b374f4c50ff 100644 --- a/docs/release-notes/.FSharp.Core/10.0.100.md +++ b/docs/release-notes/.FSharp.Core/10.0.100.md @@ -7,5 +7,6 @@ ### Changed * Random functions support for zero element chosen/sampled ([PR #18568](https://github.com/dotnet/fsharp/pull/18568)) +* Optimize array slicing performance. ([PR #18778](https://github.com/dotnet/fsharp/pull/18778)) ### Breaking Changes From 065ac264560ebe6ac2c9eacc1ba2858367d33b8d Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:07:20 -0700 Subject: [PATCH 4/7] Update src/FSharp.Core/prim-types.fs Co-authored-by: kerams --- src/FSharp.Core/prim-types.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index 69d35ae603a..6b43c445338 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -781,7 +781,7 @@ namespace Microsoft.FSharp.Core let inline GetArraySub (arr: 'a array) (start:int) (len:int) : 'a array = if len <= 0 then - zeroCreate 0 + [||] else let dst = zeroCreate len Array.Copy(arr, start, dst, 0, len) From c5060608f80e2f5db6bc01f623bb1ec323868f51 Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:36:07 -0700 Subject: [PATCH 5/7] Update ArrayModule.fs --- .../Microsoft.FSharp.Collections/ArrayModule.fs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs index 302f44e85ec..88eaa1ef3f5 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs @@ -1963,15 +1963,19 @@ type ArrayModule() = [] - member _.``Slicing always creates new arrays`` () = + member _.``Slicing creates new non-empty arrays`` () = let arr = [|1;2;3;4;5;6|] Assert.False(Object.ReferenceEquals(arr[1..3], arr[1..3])) Assert.False(Object.ReferenceEquals(arr[0..], arr[0..])) Assert.False(Object.ReferenceEquals(arr[..^1], arr[..^1])) + [] + member _.``Empty slices are referentially equivalent`` () = + let arr = [|1;2;3;4;5;6|] + Assert.AreEqual(arr[1..-1].Length, 0) - Assert.False(Object.ReferenceEquals(arr[1..-1], arr[1..-1])) + Assert.True(Object.ReferenceEquals(arr[1..-1], arr[1..-1])) [] member _.RandomShuffle() = From 18b38fad585ddd1b8b3680a88b93e1a7905eb821 Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:13:39 -0700 Subject: [PATCH 6/7] document breaking change --- docs/release-notes/.FSharp.Core/10.0.100.md | 2 ++ .../FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/release-notes/.FSharp.Core/10.0.100.md b/docs/release-notes/.FSharp.Core/10.0.100.md index b374f4c50ff..ea2f08b8fef 100644 --- a/docs/release-notes/.FSharp.Core/10.0.100.md +++ b/docs/release-notes/.FSharp.Core/10.0.100.md @@ -10,3 +10,5 @@ * Optimize array slicing performance. ([PR #18778](https://github.com/dotnet/fsharp/pull/18778)) ### Breaking Changes + +* Empty 1D array slices return `[||]` instead of allocating a new array. ([PR #18778](https://github.com/dotnet/fsharp/pull/18778)) \ No newline at end of file diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs index 88eaa1ef3f5..e09ce1758c7 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/ArrayModule.fs @@ -1976,6 +1976,9 @@ type ArrayModule() = Assert.AreEqual(arr[1..-1].Length, 0) Assert.True(Object.ReferenceEquals(arr[1..-1], arr[1..-1])) + + Assert.True(Object.ReferenceEquals(arr[1..-1], ([||]: int array))) + [] member _.RandomShuffle() = From fcd32d25e3dd7f754af508738204cadacc928c9d Mon Sep 17 00:00:00 2001 From: Albert Du <52804499+albert-du@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:27:50 -0700 Subject: [PATCH 7/7] Update 10.0.100.md --- docs/release-notes/.FSharp.Core/10.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Core/10.0.100.md b/docs/release-notes/.FSharp.Core/10.0.100.md index ea2f08b8fef..9d348a8663c 100644 --- a/docs/release-notes/.FSharp.Core/10.0.100.md +++ b/docs/release-notes/.FSharp.Core/10.0.100.md @@ -11,4 +11,4 @@ ### Breaking Changes -* Empty 1D array slices return `[||]` instead of allocating a new array. ([PR #18778](https://github.com/dotnet/fsharp/pull/18778)) \ No newline at end of file +* 1D array slicing now returns an empty array singleton instead of allocating a new array when the result is empty. ([PR #18778](https://github.com/dotnet/fsharp/pull/18778)) \ No newline at end of file