From 2301535aabcdb10c763ddeb71053f3fe2b9605f4 Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Thu, 27 Aug 2026 16:20:27 +0200 Subject: [PATCH 1/4] Share the empty-array singleton for zero-length Array results (#20382) Add a central Basics.Array.zeroCreateUncheckedOrEmpty primitive that returns the shared System.Array.Empty<_>() singleton (via the [||] literal) when count = 0, and route the reachable-zero allocation sites through it: subUnchecked (all slicing), concatArrays (concat + collect output), collect/map/mapi/indexed and their Array.Parallel counterparts. Existing 'if length = 0' guards now return [||] too. This removes the redundant zero-length array allocations reported in Array.collect (intermediate 'U[][] plus concat output) and Array.map with empty inputs, without adding per-function length checks (the single check lives in the primitive; fill loops naturally no-op at length 0). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Core/array.fs | 26 +++++++++++-------- src/FSharp.Core/local.fs | 10 ++++++- src/FSharp.Core/local.fsi | 4 +++ .../ArrayModule.fs | 22 ++++++++++++++++ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/FSharp.Core/array.fs b/src/FSharp.Core/array.fs index 967285a909b..d6ecd11fd4a 100644 --- a/src/FSharp.Core/array.fs +++ b/src/FSharp.Core/array.fs @@ -106,7 +106,7 @@ module Array = for h in arrs do acc <- acc + h.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked acc + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty acc let mutable j = 0 @@ -145,7 +145,7 @@ module Array = let len = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked<'U array> len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty<'U array> len for i = 0 to result.Length - 1 do result.[i] <- mapping array.[i] @@ -218,7 +218,7 @@ module Array = let length = array.Length if length = 0 then - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 + [||] else let dict = Dictionary comparer @@ -308,7 +308,9 @@ module Array = [] let indexed (array: 'T array) = checkNonNull "array" array - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length for i = 0 to res.Length - 1 do res.[i] <- (i, array.[i]) @@ -342,7 +344,7 @@ module Array = checkNonNull "array" array let res: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length for i = 0 to res.Length - 1 do res.[i] <- mapping array.[i] @@ -367,7 +369,7 @@ module Array = let length = array.Length if length = 0 then - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 + [||] else let temp = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length @@ -455,7 +457,9 @@ module Array = let mapi (mapping: int -> 'T -> 'U) (array: 'T array) = checkNonNull "array" array let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length for i = 0 to array.Length - 1 do res.[i] <- f.Invoke(i, array.[i]) @@ -545,7 +549,7 @@ module Array = let length = array.Length if length = 0 then - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 + [||] else let dict = Dictionary<_, ResizeArray<_>> comparer @@ -1844,7 +1848,7 @@ module Array = let len = array.Length if len = 0 then - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 + [||] else let lenInner = array.[0].Length @@ -2326,7 +2330,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- mapping array.[i])) |> ignore @@ -2339,7 +2343,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- mapping array.[i])) |> ignore diff --git a/src/FSharp.Core/local.fs b/src/FSharp.Core/local.fs index c365f9e5bdb..b764c535900 100644 --- a/src/FSharp.Core/local.fs +++ b/src/FSharp.Core/local.fs @@ -991,6 +991,14 @@ module internal Array = let inline zeroCreateUnchecked (count:int) = (# "newarr !0" type ('T) count : 'T array #) + // Returns the shared empty-array singleton when count = 0, otherwise a fresh array. + // Use only where a zero length is actually reachable at this allocation; prefer + // zeroCreateUnchecked when the caller has already established count > 0. The empty array + // literal is lowered by the compiler to System.Array.Empty<_>() (a shared per-type + // singleton), so the count = 0 case allocates nothing. + let inline zeroCreateUncheckedOrEmpty (count:int) : 'T array = + if count = 0 then [||] else zeroCreateUnchecked count + let inline init (count:int) ([] f: int -> 'T) = if count < 0 then invalidArgInputMustBeNonNegative "count" count let arr = (zeroCreateUnchecked count : 'T array) @@ -1152,7 +1160,7 @@ module internal Array = stableSortWithKeysAndComparer c c array keys let inline subUnchecked startIndex count (array: 'T array) = - let res = zeroCreateUnchecked count : 'T array + let res = zeroCreateUncheckedOrEmpty count : 'T array if count < 64 then for i = 0 to res.Length-1 do res.[i] <- array.[startIndex+i] diff --git a/src/FSharp.Core/local.fsi b/src/FSharp.Core/local.fsi index 11f645c9788..9e7089997d7 100644 --- a/src/FSharp.Core/local.fsi +++ b/src/FSharp.Core/local.fsi @@ -87,6 +87,10 @@ module internal Array = // The input parameter should be checked by callers if necessary val inline zeroCreateUnchecked: int -> 'T array + // Returns the shared empty-array singleton when count = 0, otherwise a fresh array. + // Use only where a zero length is actually reachable at this allocation. + val inline zeroCreateUncheckedOrEmpty: int -> 'T array + val inline init: int -> (int -> 'T) -> 'T array val splitInto: int -> 'T array -> 'T array array 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 ba6e001fd31..7bec7f2031f 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 @@ -461,6 +461,28 @@ type ArrayModule() = member this.``Parallel.Collect`` () = this.CollectTester Array.Parallel.collect Array.Parallel.collect + [] + member _.EmptyResultsShareSingleton () = + // A zero-length result should reuse the shared Array.empty singleton rather than + // allocating a fresh zero-length array (see issue #20382). + let isShared (a: 'a[]) = obj.ReferenceEquals(a, Array.empty<'a>) + let ints: int[] = [||] + + Assert.True(isShared (Array.collect (fun x -> [| x |]) ints), "collect (empty input)") + Assert.True(isShared (Array.collect (fun _ -> [||]) [| 1; 2; 3 |]), "collect (all-empty results)") + Assert.True(isShared (Array.map (fun x -> x + 1) ints), "map") + Assert.True(isShared (Array.mapi (fun i x -> i + x) ints), "mapi") + Assert.True(isShared (Array.indexed ints), "indexed") + Assert.True(isShared (Array.concat ([]: int[] list)), "concat (empty)") + Assert.True(isShared (Array.concat [ [||]; [||] ]), "concat (of empties)") + Assert.True(isShared (Array.take 0 [| 1; 2; 3 |]), "take 0") + Assert.True(isShared (Array.skip 3 [| 1; 2; 3 |]), "skip all") + Assert.True(isShared (Array.sub [| 1; 2; 3 |] 1 0), "sub len 0") + Assert.True(isShared (Array.distinctBy id ints), "distinctBy") + Assert.True(isShared (Array.groupBy id ints), "groupBy") + Assert.True(isShared (Array.Parallel.map (fun x -> x + 1) ints), "Parallel.map") + Assert.True(isShared (Array.Parallel.collect (fun x -> [| x |]) ints), "Parallel.collect") + [] member this.compareWith() = // compareWith should work on empty arrays From ea0bc4368c3737fb7f6262013454e71da82b2add Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Thu, 27 Aug 2026 16:21:32 +0200 Subject: [PATCH 2/4] Add FSharp.Core release note for #20382 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Core/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 521901da210..058de7f94b9 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -23,6 +23,7 @@ * Deduplicate repeated XML documentation (sort stability/complexity, `Parallel.For`, dynamic entry points) via the compile-time `` tag; generated FSharp.Core.xml is unchanged. ([PR #20231](https://github.com/dotnet/fsharp/pull/20231)) * Added modules for `Async`, `Task` and `ValueTask` with consistent `result`, `map`, `bind`, `ignore`, `catchWith`, `catch`, and `empty` functions ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) +* `Array.collect`, `Array.map`, `Array.concat`, the slicing functions and their `Array.Parallel` counterparts now return the shared empty-array singleton instead of allocating a fresh zero-length array when the result is empty. ([Issue #20382](https://github.com/dotnet/fsharp/issues/20382), [PR #20388](https://github.com/dotnet/fsharp/pull/20388)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.StartTaskImmediate`: passes the ambient `Async.CancellationToken` to a task factory, then await the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) * Add `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) From f28912c6052abe8188a21a44df11e4f88e07cac4 Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Thu, 27 Aug 2026 23:17:28 +0200 Subject: [PATCH 3/4] Extend empty-singleton sharing to the remaining Array leaf builders Route the remaining reachable-zero allocation sites through zeroCreateUncheckedOrEmpty: append, distinct, map2/mapi2/map3, partition, partitionWith (+ scatterPartitioned), zip/zip3, allPairs, unzip/unzip3, rev, removeAt/removeManyAt, transpose, and the Array.Parallel choose/mapi/zip/filter/ partition/partitionWith/groupBy builders. Count-based constructors (zeroCreate/ create/init/replicate/random*), the sort helpers and provably-nonzero sites keep the branch-free zeroCreateUnchecked. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Core/11.0.100.md | 2 +- src/FSharp.Core/array.fs | 98 ++++++++++++------- .../ArrayModule.fs | 29 ++++++ 3 files changed, 90 insertions(+), 39 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 058de7f94b9..c05e2872ac8 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -23,7 +23,7 @@ * Deduplicate repeated XML documentation (sort stability/complexity, `Parallel.For`, dynamic entry points) via the compile-time `` tag; generated FSharp.Core.xml is unchanged. ([PR #20231](https://github.com/dotnet/fsharp/pull/20231)) * Added modules for `Async`, `Task` and `ValueTask` with consistent `result`, `map`, `bind`, `ignore`, `catchWith`, `catch`, and `empty` functions ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) -* `Array.collect`, `Array.map`, `Array.concat`, the slicing functions and their `Array.Parallel` counterparts now return the shared empty-array singleton instead of allocating a fresh zero-length array when the result is empty. ([Issue #20382](https://github.com/dotnet/fsharp/issues/20382), [PR #20388](https://github.com/dotnet/fsharp/pull/20388)) +* Many `Array` and `Array.Parallel` functions (`collect`, `map`/`map2`/`map3`, `concat`, `append`, `zip`/`unzip`, `allPairs`, `partition`, `filter`, `distinct`, `rev`, `removeAt`, the slicing functions, and more) now return the shared empty-array singleton instead of allocating a fresh zero-length array when the result is empty. ([Issue #20382](https://github.com/dotnet/fsharp/issues/20382), [PR #20388](https://github.com/dotnet/fsharp/pull/20388)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.StartTaskImmediate`: passes the ambient `Async.CancellationToken` to a task factory, then await the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) * Add `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) diff --git a/src/FSharp.Core/array.fs b/src/FSharp.Core/array.fs index d6ecd11fd4a..65d19fd811b 100644 --- a/src/FSharp.Core/array.fs +++ b/src/FSharp.Core/array.fs @@ -271,7 +271,7 @@ module Array = let n2 = array2.Length let res: 'T array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (n1 + n2) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (n1 + n2) Array.Copy(array1, 0, res, 0, n1) Array.Copy(array2, 0, res, n1, n2) @@ -327,7 +327,10 @@ module Array = [] let distinct (array: 'T array) = checkNonNull "array" array - let temp = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + + let temp = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + let mutable i = 0 let hashSet = HashSet<'T>(HashIdentity.Structural<'T>) @@ -392,7 +395,8 @@ module Array = if array1.Length <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array1.Length + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array1.Length for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(array1.[i], array2.[i]) @@ -410,7 +414,7 @@ module Array = if len1 <> array2.Length || len1 <> array3.Length then invalidArg3ArraysDifferent "array1" "array2" "array3" len1 array2.Length array3.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(array1.[i], array2.[i], array3.[i]) @@ -426,7 +430,8 @@ module Array = if array1.Length <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array1.Length + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array1.Length for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(i, array1.[i], array2.[i]) @@ -1089,7 +1094,10 @@ module Array = [] let partition predicate (array: _ array) = checkNonNull "array" array - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + let mutable upCount = 0 let mutable downCount = array.Length - 1 @@ -1104,7 +1112,7 @@ module Array = let res1 = Microsoft.FSharp.Primitives.Basics.Array.subUnchecked 0 upCount res let res2 = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (array.Length - upCount) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (array.Length - upCount) downCount <- array.Length - 1 @@ -1118,10 +1126,10 @@ module Array = let len = isChoice1.Length let output1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count1 + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty count1 let output2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len - count1) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (len - count1) let mutable i1 = 0 let mutable i2 = 0 @@ -1142,13 +1150,13 @@ module Array = let len = array.Length let isChoice1: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let results1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let results2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let mutable count1 = 0 @@ -1304,7 +1312,7 @@ module Array = if len1 <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 for i = 0 to res.Length - 1 do res.[i] <- (array1.[i], array2.[i]) @@ -1321,7 +1329,7 @@ module Array = if len1 <> array2.Length || len1 <> array3.Length then invalidArg3ArraysDifferent "array1" "array2" "array3" len1 array2.Length array3.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 for i = 0 to res.Length - 1 do res.[i] <- (array1.[i], array2.[i], array3.[i]) @@ -1334,7 +1342,9 @@ module Array = checkNonNull "array2" array2 let len1 = array1.Length let len2 = array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len1 * len2) + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (len1 * len2) for i = 0 to array1.Length - 1 do for j = 0 to array2.Length - 1 do @@ -1360,8 +1370,8 @@ module Array = let unzip (array: _ array) = checkNonNull "array" array let len = array.Length - let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len - let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len for i = 0 to array.Length - 1 do let x, y = array.[i] @@ -1374,9 +1384,9 @@ module Array = let unzip3 (array: _ array) = checkNonNull "array" array let len = array.Length - let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len - let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len - let res3 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + let res3 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len for i = 0 to array.Length - 1 do let x, y, z = array.[i] @@ -1389,7 +1399,10 @@ module Array = [] let rev (array: _ array) = checkNonNull "array" array - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + let mutable j = array.Length - 1 for i = 0 to array.Length - 1 do @@ -1861,7 +1874,7 @@ module Array = array.[j].Length let result: 'T array array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked lenInner + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty lenInner for i in 0 .. lenInner - 1 do result.[i] <- Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len @@ -1898,7 +1911,9 @@ module Array = invalidArg "index" "index must be within bounds of the array" let length = source.Length - 1 - let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked length + + let result = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty length if index > 0 then Array.Copy(source, result, index) @@ -1916,7 +1931,9 @@ module Array = invalidArg "index" "index must be within bounds of the array" let length = source.Length - count - let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked length + + let result = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty length if index > 0 then Array.Copy(source, result, index) @@ -2290,10 +2307,10 @@ module Array = let inputLength = array.Length let isChosen: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength let results: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength let mutable outputLength = 0 @@ -2313,7 +2330,7 @@ module Array = |> ignore let output = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked outputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty outputLength let mutable curr = 0 @@ -2357,7 +2374,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- f.Invoke(i, array.[i]))) |> ignore @@ -2487,7 +2504,7 @@ module Array = if len1 <> array2.Length then invalidArgDifferentArrayLength "array1" len1 "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 let inputChunks = createPartitionsUpTo array1.Length array1 Parallel.For( @@ -2519,7 +2536,7 @@ module Array = let valueFactory = Func<_, _>(fun _ -> ref 0) let projectedValues = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length let inputChunks = createPartitionsUpTo array.Length array @@ -2538,7 +2555,7 @@ module Array = |> ignore let finalResults = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked counts.Count + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty counts.Count let mutable finalIdx = 0 @@ -2621,7 +2638,7 @@ module Array = let inputLength = array.Length let isTrue = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength let mutable trueLength = 0 @@ -2644,7 +2661,10 @@ module Array = [] let filter predicate (array: 'T array) = let trueLength, isTrue = countAndCollectTrueItems predicate array - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked trueLength + + let res = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty trueLength + let mutable resIdx = 0 for i = 0 to isTrue.Length - 1 do @@ -2657,10 +2677,12 @@ module Array = [] let partition predicate (array: 'T array) = let trueLength, isTrue = countAndCollectTrueItems predicate array - let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked trueLength + + let res1 = + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty trueLength let res2 = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (array.Length - trueLength) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (array.Length - trueLength) let mutable iTrue = 0 let mutable iFalse = 0 @@ -2681,13 +2703,13 @@ module Array = let len = array.Length let isChoice1: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let results1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let results2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len let mutable count1 = 0 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 7bec7f2031f..fe514c2b6a0 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 @@ -483,6 +483,35 @@ type ArrayModule() = Assert.True(isShared (Array.Parallel.map (fun x -> x + 1) ints), "Parallel.map") Assert.True(isShared (Array.Parallel.collect (fun x -> [| x |]) ints), "Parallel.collect") + // Leaf builders that previously allocated a throwaway zero-length array for empty inputs. + Assert.True(isShared (Array.append ints ints), "append") + Assert.True(isShared (Array.map2 (fun a b -> a + b) ints ints), "map2") + Assert.True(isShared (Array.mapi2 (fun i a b -> i + a + b) ints ints), "mapi2") + Assert.True(isShared (Array.map3 (fun a b c -> a + b + c) ints ints ints), "map3") + Assert.True(isShared (Array.zip ints ints), "zip") + Assert.True(isShared (Array.zip3 ints ints ints), "zip3") + Assert.True(isShared (Array.allPairs ints ints), "allPairs") + Assert.True(isShared (Array.rev ints), "rev") + Assert.True(isShared (Array.distinct ints), "distinct") + Assert.True(isShared (Array.removeAt 0 [| 1 |]), "removeAt last") + Assert.True(isShared (Array.removeManyAt 0 1 [| 1 |]), "removeManyAt last") + Assert.True(isShared (Array.transpose [| ([||]: int[]) |]), "transpose (empty inner)") + + let p1, p2 = Array.partition (fun _ -> true) ints + Assert.True(isShared p1 && isShared p2, "partition") + let u1, u2 = Array.unzip ([||]: (int * int)[]) + Assert.True(isShared u1 && isShared u2, "unzip") + let t1, t2, t3 = Array.unzip3 ([||]: (int * int * int)[]) + Assert.True(isShared t1 && isShared t2 && isShared t3, "unzip3") + + Assert.True(isShared (Array.Parallel.mapi (fun i x -> i + x) ints), "Parallel.mapi") + Assert.True(isShared (Array.Parallel.choose Some ints), "Parallel.choose") + Assert.True(isShared (Array.Parallel.zip ints ints), "Parallel.zip") + Assert.True(isShared (Array.Parallel.filter (fun _ -> true) ints), "Parallel.filter") + Assert.True(isShared (Array.Parallel.groupBy id ints), "Parallel.groupBy") + let pp1, pp2 = Array.Parallel.partition (fun _ -> true) ints + Assert.True(isShared pp1 && isShared pp2, "Parallel.partition") + [] member this.compareWith() = // compareWith should work on empty arrays From a0cb3f5e00a64a15b3180a2a92450942a3653f73 Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Fri, 28 Aug 2026 00:49:48 +0200 Subject: [PATCH 4/4] Flip: make empty-array sharing the default allocation behavior Invert the primitive: zeroCreateUnchecked now shares the empty-array singleton when count = 0 (the improve-by-default behavior), and a new zeroCreateUncheckedNonEmpty keeps the branch-free newarr for the ~18 sites where the length is provably > 0 (countBy/groupBy/choose chunks, filter, windowed, chunkBySize, insert/update, reduceBy, scanSubRight, splitInto, ...). This shrinks the diff and makes every reachable-zero site - including the count-based constructors (zeroCreate/create/init/replicate) - share the singleton for free, instead of each caller opting in. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Core/11.0.100.md | 2 +- src/FSharp.Core/array.fs | 144 +++++++----------- src/FSharp.Core/local.fs | 26 ++-- src/FSharp.Core/local.fsi | 9 +- .../ArrayModule.fs | 6 + 5 files changed, 84 insertions(+), 103 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index c05e2872ac8..004c714ba70 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -23,7 +23,7 @@ * Deduplicate repeated XML documentation (sort stability/complexity, `Parallel.For`, dynamic entry points) via the compile-time `` tag; generated FSharp.Core.xml is unchanged. ([PR #20231](https://github.com/dotnet/fsharp/pull/20231)) * Added modules for `Async`, `Task` and `ValueTask` with consistent `result`, `map`, `bind`, `ignore`, `catchWith`, `catch`, and `empty` functions ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) -* Many `Array` and `Array.Parallel` functions (`collect`, `map`/`map2`/`map3`, `concat`, `append`, `zip`/`unzip`, `allPairs`, `partition`, `filter`, `distinct`, `rev`, `removeAt`, the slicing functions, and more) now return the shared empty-array singleton instead of allocating a fresh zero-length array when the result is empty. ([Issue #20382](https://github.com/dotnet/fsharp/issues/20382), [PR #20388](https://github.com/dotnet/fsharp/pull/20388)) +* Most `Array` and `Array.Parallel` functions now return the shared empty-array singleton instead of allocating a fresh zero-length array when the result is empty (`collect`, `map`, `concat`, `append`, `zip`/`unzip`, `partition`, `filter`, `distinct`, the slicing functions, `zeroCreate`/`create`/`init` at length 0, and more). ([Issue #20382](https://github.com/dotnet/fsharp/issues/20382), [PR #20388](https://github.com/dotnet/fsharp/pull/20388)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.StartTaskImmediate`: passes the ambient `Async.CancellationToken` to a task factory, then await the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) * Add `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) diff --git a/src/FSharp.Core/array.fs b/src/FSharp.Core/array.fs index 65d19fd811b..d8a3b2086c4 100644 --- a/src/FSharp.Core/array.fs +++ b/src/FSharp.Core/array.fs @@ -106,7 +106,7 @@ module Array = for h in arrs do acc <- acc + h.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty acc + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked acc let mutable j = 0 @@ -145,7 +145,7 @@ module Array = let len = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty<'U array> len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked<'U array> len for i = 0 to result.Length - 1 do result.[i] <- mapping array.[i] @@ -218,7 +218,7 @@ module Array = let length = array.Length if length = 0 then - [||] + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 else let dict = Dictionary comparer @@ -271,7 +271,7 @@ module Array = let n2 = array2.Length let res: 'T array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (n1 + n2) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (n1 + n2) Array.Copy(array1, 0, res, 0, n1) Array.Copy(array2, 0, res, n1, n2) @@ -308,9 +308,7 @@ module Array = [] let indexed (array: 'T array) = checkNonNull "array" array - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length for i = 0 to res.Length - 1 do res.[i] <- (i, array.[i]) @@ -327,10 +325,7 @@ module Array = [] let distinct (array: 'T array) = checkNonNull "array" array - - let temp = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length - + let temp = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length let mutable i = 0 let hashSet = HashSet<'T>(HashIdentity.Structural<'T>) @@ -347,7 +342,7 @@ module Array = checkNonNull "array" array let res: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length for i = 0 to res.Length - 1 do res.[i] <- mapping array.[i] @@ -372,7 +367,7 @@ module Array = let length = array.Length if length = 0 then - [||] + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 else let temp = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length @@ -395,8 +390,7 @@ module Array = if array1.Length <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array1.Length + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array1.Length for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(array1.[i], array2.[i]) @@ -414,7 +408,7 @@ module Array = if len1 <> array2.Length || len1 <> array3.Length then invalidArg3ArraysDifferent "array1" "array2" "array3" len1 array2.Length array3.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(array1.[i], array2.[i], array3.[i]) @@ -430,8 +424,7 @@ module Array = if array1.Length <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array1.Length + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array1.Length for i = 0 to res.Length - 1 do res.[i] <- f.Invoke(i, array1.[i], array2.[i]) @@ -462,9 +455,7 @@ module Array = let mapi (mapping: int -> 'T -> 'U) (array: 'T array) = checkNonNull "array" array let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length for i = 0 to array.Length - 1 do res.[i] <- f.Invoke(i, array.[i]) @@ -554,7 +545,7 @@ module Array = let length = array.Length if length = 0 then - [||] + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 else let dict = Dictionary<_, ResizeArray<_>> comparer @@ -650,7 +641,7 @@ module Array = if i <> array.Length then let chunk1: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked ((array.Length >>> 2) + 1) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty ((array.Length >>> 2) + 1) chunk1.[0] <- first let mutable count = 1 @@ -669,7 +660,7 @@ module Array = if i < array.Length then let chunk2: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (array.Length - i) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty (array.Length - i) count <- 0 @@ -685,7 +676,7 @@ module Array = i <- i + 1 let res: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (chunk1.Length + count) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty (chunk1.Length + count) Array.Copy(chunk1, res, chunk1.Length) Array.Copy(chunk2, 0, res, chunk1.Length, count) @@ -857,7 +848,7 @@ module Array = if maskArrayLength = 0 then null else - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked maskArrayLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty maskArrayLength let mutable count = match maskArray with @@ -1041,7 +1032,7 @@ module Array = let private filterViaMask (maskArray: uint32 array | null) (leftoverMask: uint32) (count: int) (src: _ array) = - let dst = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count + let dst = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty count let mutable dstIdx = 0 @@ -1094,10 +1085,7 @@ module Array = [] let partition predicate (array: _ array) = checkNonNull "array" array - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length - + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length let mutable upCount = 0 let mutable downCount = array.Length - 1 @@ -1112,7 +1100,7 @@ module Array = let res1 = Microsoft.FSharp.Primitives.Basics.Array.subUnchecked 0 upCount res let res2 = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (array.Length - upCount) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (array.Length - upCount) downCount <- array.Length - 1 @@ -1126,10 +1114,10 @@ module Array = let len = isChoice1.Length let output1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty count1 + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count1 let output2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (len - count1) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len - count1) let mutable i1 = 0 let mutable i2 = 0 @@ -1150,13 +1138,13 @@ module Array = let len = array.Length let isChoice1: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let results1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let results2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let mutable count1 = 0 @@ -1256,7 +1244,7 @@ module Array = empty else let res: 'T array array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len - windowSize + 1) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty (len - windowSize + 1) for i = 0 to len - windowSize do res.[i] <- Microsoft.FSharp.Primitives.Basics.Array.subUnchecked i windowSize array @@ -1280,7 +1268,7 @@ module Array = let chunkCount = (len - 1) / chunkSize + 1 let res: 'T array array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked chunkCount + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty chunkCount for i = 0 to len / chunkSize - 1 do res.[i] <- Microsoft.FSharp.Primitives.Basics.Array.subUnchecked (i * chunkSize) chunkSize array @@ -1312,7 +1300,7 @@ module Array = if len1 <> array2.Length then invalidArgDifferentArrayLength "array1" array1.Length "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 for i = 0 to res.Length - 1 do res.[i] <- (array1.[i], array2.[i]) @@ -1329,7 +1317,7 @@ module Array = if len1 <> array2.Length || len1 <> array3.Length then invalidArg3ArraysDifferent "array1" "array2" "array3" len1 array2.Length array3.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 for i = 0 to res.Length - 1 do res.[i] <- (array1.[i], array2.[i], array3.[i]) @@ -1342,9 +1330,7 @@ module Array = checkNonNull "array2" array2 let len1 = array1.Length let len2 = array2.Length - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (len1 * len2) + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len1 * len2) for i = 0 to array1.Length - 1 do for j = 0 to array2.Length - 1 do @@ -1370,8 +1356,8 @@ module Array = let unzip (array: _ array) = checkNonNull "array" array let len = array.Length - let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len - let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len for i = 0 to array.Length - 1 do let x, y = array.[i] @@ -1384,9 +1370,9 @@ module Array = let unzip3 (array: _ array) = checkNonNull "array" array let len = array.Length - let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len - let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len - let res3 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + let res2 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + let res3 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len for i = 0 to array.Length - 1 do let x, y, z = array.[i] @@ -1399,10 +1385,7 @@ module Array = [] let rev (array: _ array) = checkNonNull "array" array - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length - + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length let mutable j = array.Length - 1 for i = 0 to array.Length - 1 do @@ -1861,7 +1844,7 @@ module Array = let len = array.Length if len = 0 then - [||] + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 else let lenInner = array.[0].Length @@ -1874,10 +1857,10 @@ module Array = array.[j].Length let result: 'T array array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty lenInner + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked lenInner for i in 0 .. lenInner - 1 do - result.[i] <- Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + result.[i] <- Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty len for j in 0 .. len - 1 do result.[i].[j] <- array.[j].[i] @@ -1911,9 +1894,7 @@ module Array = invalidArg "index" "index must be within bounds of the array" let length = source.Length - 1 - - let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty length + let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked length if index > 0 then Array.Copy(source, result, index) @@ -1931,9 +1912,7 @@ module Array = invalidArg "index" "index must be within bounds of the array" let length = source.Length - count - - let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty length + let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked length if index > 0 then Array.Copy(source, result, index) @@ -2307,10 +2286,10 @@ module Array = let inputLength = array.Length let isChosen: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength let results: 'U array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength let mutable outputLength = 0 @@ -2330,7 +2309,7 @@ module Array = |> ignore let output = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty outputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked outputLength let mutable curr = 0 @@ -2347,7 +2326,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- mapping array.[i])) |> ignore @@ -2360,7 +2339,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- mapping array.[i])) |> ignore @@ -2374,7 +2353,7 @@ module Array = let inputLength = array.Length let result = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength Parallel.For(0, inputLength, (fun i -> result.[i] <- f.Invoke(i, array.[i]))) |> ignore @@ -2418,7 +2397,7 @@ module Array = let chunks = createPartitionsUpToWithMinChunkSize array.Length 2 array // We need at least 2 elements/chunk for 'reduction' let chunkResults = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked chunks.Length + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty chunks.Length Parallel.For( 0, @@ -2504,7 +2483,7 @@ module Array = if len1 <> array2.Length then invalidArgDifferentArrayLength "array1" len1 "array2" array2.Length - let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len1 + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len1 let inputChunks = createPartitionsUpTo array1.Length array1 Parallel.For( @@ -2536,7 +2515,7 @@ module Array = let valueFactory = Func<_, _>(fun _ -> ref 0) let projectedValues = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty array.Length + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length let inputChunks = createPartitionsUpTo array.Length array @@ -2555,7 +2534,7 @@ module Array = |> ignore let finalResults = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty counts.Count + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked counts.Count let mutable finalIdx = 0 @@ -2564,7 +2543,7 @@ module Array = for kvp in counts do let arrayForThisGroup = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked kvp.Value.Value + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedNonEmpty kvp.Value.Value finalResults.[finalIdx] <- getKey kvp.Key, arrayForThisGroup finalResultsLookup[kvp.Key] <- kvp.Value, arrayForThisGroup @@ -2638,7 +2617,7 @@ module Array = let inputLength = array.Length let isTrue = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty inputLength + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked inputLength let mutable trueLength = 0 @@ -2661,10 +2640,7 @@ module Array = [] let filter predicate (array: 'T array) = let trueLength, isTrue = countAndCollectTrueItems predicate array - - let res = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty trueLength - + let res = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked trueLength let mutable resIdx = 0 for i = 0 to isTrue.Length - 1 do @@ -2677,12 +2653,10 @@ module Array = [] let partition predicate (array: 'T array) = let trueLength, isTrue = countAndCollectTrueItems predicate array - - let res1 = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty trueLength + let res1 = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked trueLength let res2 = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty (array.Length - trueLength) + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (array.Length - trueLength) let mutable iTrue = 0 let mutable iFalse = 0 @@ -2703,13 +2677,13 @@ module Array = let len = array.Length let isChoice1: bool array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let results1: 'T1 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let results2: 'T2 array = - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUncheckedOrEmpty len + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len let mutable count1 = 0 diff --git a/src/FSharp.Core/local.fs b/src/FSharp.Core/local.fs index b764c535900..5b96bda4bfd 100644 --- a/src/FSharp.Core/local.fs +++ b/src/FSharp.Core/local.fs @@ -987,17 +987,19 @@ module internal Array = let inline fastComparerForArraySort<'t when 't : comparison> () = LanguagePrimitives.FastGenericComparerCanBeNull<'t> - // The input parameter should be checked by callers if necessary - let inline zeroCreateUnchecked (count:int) = + // Allocates a new array of the given length without validating the count, always returning + // a freshly allocated array (even for count = 0). Prefer zeroCreateUnchecked unless the + // length is provably > 0; this variant exists only to skip the empty-array branch on hot, + // known-non-empty paths. + let inline zeroCreateUncheckedNonEmpty (count: int) = (# "newarr !0" type ('T) count : 'T array #) - // Returns the shared empty-array singleton when count = 0, otherwise a fresh array. - // Use only where a zero length is actually reachable at this allocation; prefer - // zeroCreateUnchecked when the caller has already established count > 0. The empty array - // literal is lowered by the compiler to System.Array.Empty<_>() (a shared per-type - // singleton), so the count = 0 case allocates nothing. - let inline zeroCreateUncheckedOrEmpty (count:int) : 'T array = - if count = 0 then [||] else zeroCreateUnchecked count + // Allocates a new array of the given length without validating the count, but shares the + // empty-array singleton (System.Array.Empty<_>(), which the compiler emits for the [||] + // literal) when count = 0, so empty results allocate nothing. This is the default; use + // zeroCreateUncheckedNonEmpty only where the length is provably > 0 to skip the branch. + let inline zeroCreateUnchecked (count: int) : 'T array = + if count = 0 then [||] else zeroCreateUncheckedNonEmpty count let inline init (count:int) ([] f: int -> 'T) = if count < 0 then invalidArgInputMustBeNonNegative "count" count @@ -1077,7 +1079,7 @@ module internal Array = let scanSubRight f (array: _ array) start fin initState = let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(f) let mutable state = initState - let res = zeroCreateUnchecked (fin-start+2) + let res = zeroCreateUncheckedNonEmpty (fin-start+2) res.[fin - start + 1] <- state for i = fin downto start do state <- f.Invoke(array.[i], state) @@ -1160,7 +1162,7 @@ module internal Array = stableSortWithKeysAndComparer c c array keys let inline subUnchecked startIndex count (array: 'T array) = - let res = zeroCreateUncheckedOrEmpty count : 'T array + let res = zeroCreateUnchecked count : 'T array if count < 64 then for i = 0 to res.Length-1 do res.[i] <- array.[startIndex+i] @@ -1174,7 +1176,7 @@ module internal Array = [| |] else let count = min count len - let res = zeroCreateUnchecked count : 'T array array + let res = zeroCreateUncheckedNonEmpty count : 'T array array let minChunkSize = len / count let mutable startIndex = 0 for i = 0 to len % count - 1 do diff --git a/src/FSharp.Core/local.fsi b/src/FSharp.Core/local.fsi index 9e7089997d7..9423b461837 100644 --- a/src/FSharp.Core/local.fsi +++ b/src/FSharp.Core/local.fsi @@ -84,12 +84,11 @@ module internal List = val tryLastV: 'T list -> 'T ValueOption module internal Array = - // The input parameter should be checked by callers if necessary - val inline zeroCreateUnchecked: int -> 'T array + // Allocates without validating the count; always a fresh array. Use only where count > 0. + val inline zeroCreateUncheckedNonEmpty: int -> 'T array - // Returns the shared empty-array singleton when count = 0, otherwise a fresh array. - // Use only where a zero length is actually reachable at this allocation. - val inline zeroCreateUncheckedOrEmpty: int -> 'T array + // Allocates without validating the count, sharing the empty-array singleton when count = 0. + val inline zeroCreateUnchecked: int -> 'T array val inline init: int -> (int -> 'T) -> 'T array 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 fe514c2b6a0..a2d297ca213 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 @@ -512,6 +512,12 @@ type ArrayModule() = let pp1, pp2 = Array.Parallel.partition (fun _ -> true) ints Assert.True(isShared pp1 && isShared pp2, "Parallel.partition") + // Improve-by-default: even the count-based constructors share the singleton at length 0. + Assert.True(isShared (Array.zeroCreate 0: int[]), "zeroCreate 0") + Assert.True(isShared (Array.create 0 0), "create 0") + Assert.True(isShared (Array.init 0 id), "init 0") + Assert.True(isShared (Array.replicate 0 0), "replicate 0") + [] member this.compareWith() = // compareWith should work on empty arrays