diff --git a/src/FSharp.Collections.Immutable/Helper/Buffers.fs b/src/FSharp.Collections.Immutable/Helper/Buffers.fs index bea7f019..5d0fd533 100644 --- a/src/FSharp.Collections.Immutable/Helper/Buffers.fs +++ b/src/FSharp.Collections.Immutable/Helper/Buffers.fs @@ -123,3 +123,57 @@ type ChosenBuffer<'T> (bound : int) = this.Release () result | spilled -> ImmutableList.Create<'T>(Span<'T>.op_Implicit(CollectionsMarshal.AsSpan spilled)) + +/// +/// Builds the indexed list of the elements that a function writes into an array of a known length, and returns what the +/// function returns as well. +/// +/// The type of the elements. +/// The type of what the function returns. +/// The number of elements. +/// +/// Writes the elements at the indexes 0 to - 1 of the array that it gets, which can be longer. +/// +/// The indexed list, and the value that returned. +/// +/// +/// The array is a new one below elements, and one rented +/// from the pool above, which is cleared as far as it was written when it holds references and returned. +/// +/// +/// A function that throws does not return the rented array. The pool allocates another one, and nothing leaks. +/// +/// +let ofFilledWith (length : int) (fill : 'T array -> 'R) : struct (ImmutableList<'T> * 'R) = + if length < PooledFrom then + let items : 'T array = + if length = 0 then + Array.Empty<'T>() + else + Array.zeroCreate length + let extra = fill items + struct (ImmutableList.Create<'T>(ReadOnlySpan<'T>(items)), extra) + else + let pool = ArrayPool<'T>.Shared + let items = pool.Rent length + let extra = fill items + let result = ImmutableList.Create<'T>(ReadOnlySpan<'T>(items, 0, length)) + + if RuntimeHelpers.IsReferenceOrContainsReferences<'T>() then + Array.Clear (items, 0, length) + + pool.Return (items, false) + struct (result, extra) + +/// +/// Builds the indexed list of the elements that a function writes into an array of a known length. +/// +/// The type of the elements. +/// The number of elements. +/// +/// Writes the elements at the indexes 0 to - 1 of the array that it gets, which can be longer. +/// +/// The indexed list. +let ofFilled (length : int) (fill : 'T array -> unit) : ImmutableList<'T> = + let struct (result, ()) = ofFilledWith length fill + result diff --git a/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs b/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs index d05f05fa..2eea1473 100644 --- a/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs +++ b/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs @@ -1,7 +1,6 @@ [] module FSharp.Collections.Immutable.Tuple.IIndexedList -open System.Collections.Immutable open FSharp.Collections.Immutable // The functions that build a tuple per element share their algorithm with the default module and only pass another @@ -29,31 +28,31 @@ let (|Cons|_|) (list : IIndexedList<'T>) = tryUncons list [] let allPairs (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) = Arrays.allPairs referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let indexed (list : IIndexedList<'T>) = Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let pairwise (list : IIndexedList<'T>) = Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let unzip (list : IIndexedList<'T1 * 'T2>) = let struct (first, second) = Arrays.unzip (fun (item1, item2) -> struct (item1, item2)) (Snapshot.ofSeq (nameof list) list) - ImmutableList.CreateRange first, ImmutableList.CreateRange second + IndexedList.ofArray first, IndexedList.ofArray second [] let unzip3 (list : IIndexedList<'T1 * 'T2 * 'T3>) = let struct (first, second, third) = Arrays.unzip3 (fun (item1, item2, item3) -> struct (item1, item2, item3)) (Snapshot.ofSeq (nameof list) list) - ImmutableList.CreateRange first, ImmutableList.CreateRange second, ImmutableList.CreateRange third + IndexedList.ofArray first, IndexedList.ofArray second, IndexedList.ofArray third [] let zip (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) = @@ -62,7 +61,7 @@ let zip (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) = ListChecks.sameLengths (nameof list1) list1.Count (nameof list2) list2.Count Arrays.zip referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let zip3 (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) (list3 : IIndexedList<'T3>) = @@ -76,7 +75,7 @@ let zip3 (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) (list3 : IIndex (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) (Snapshot.ofSeq (nameof list3) list3) - |> ImmutableList.CreateRange + |> IndexedList.ofArray // ---- Folding ---- @@ -103,12 +102,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IIndexedLis [] let countBy (projection : 'T -> 'Key) (list : IIndexedList<'T>) = Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let groupBy (projection : 'T -> 'Key) (list : IIndexedList<'T>) = - Arrays.groupBy (fun key items -> key, ImmutableList.CreateRange items) projection (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + Arrays.groupBy (fun key items -> key, IndexedList.ofArray items) projection (Snapshot.ofSeq (nameof list) list) + |> IndexedList.ofArray [] let partition predicate (list : IIndexedList<'T>) = diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs index 5c09ce2a..22f59aef 100644 --- a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs +++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs @@ -10,8 +10,10 @@ open System.Linq // snapshot of the elements in an array, apply an Array function or a shared algorithm and build a new tree from the fresh // result (ADR 0001, D5). The checks of ListChecks throw what the List functions throw (D6). -/// An indexed list of the elements of a fresh array that nobody changes any more; the tree copies the elements. -let inline private ofFreshArray (array : 'T array) = ImmutableList.CreateRange array +/// An indexed list of the elements of a fresh array that nobody changes any more; the tree copies the elements. The tree is +/// built from a span of the array: CreateRange reads the array through IReadOnlyList and checks the range at every node, +/// and the build from a span took 12 to 22 % less time for arrays of 100 to a million strings, and the same time for integers. +let inline private ofFreshArray (array : 'T array) = ImmutableList.Create<'T>(ReadOnlySpan<'T>(array)) /// A range of an indexed list: the empty list or the list itself without a new tree, otherwise a new tree. The range is /// copied into an array and the tree is built from the array: GetRange reads the elements of the range one by one from @@ -23,9 +25,7 @@ let private range (list : IndexedList<'T>) (start : int) (count : int) = elif count = list.Count then list else - let items = Array.zeroCreate count - list.CopyTo (start, items, 0, count) - ofFreshArray items + Buffers.ofFilled count (fun items -> list.CopyTo (start, items, 0, count)) /// The first value that the chooser returns for the elements, in their order. let private firstChoice (chooser : 'T -> 'U voption) (list : IndexedList<'T>) = Walk.tryPick chooser list @@ -57,12 +57,17 @@ let empty<'T> : IndexedList<'T> = ImmutableList<'T>.Empty let init length (initializer : int -> 'T) = // List.init reports the length under the name "count" ListChecks.nonNegative "count" length - Array.init length initializer |> ofFreshArray + Buffers.ofFilled + length + (fun items -> + for index in 0 .. length - 1 do + items[index] <- initializer index + ) [] let ofArray (array : 'T array) = checkNotNull (nameof array) array - ImmutableList.CreateRange array + ofFreshArray array [] let ofList (list : 'T list) = @@ -90,7 +95,9 @@ let ofSpan (span : Span<'T>) = ImmutableList.Create<'T>(Span<'T>.op_Implicit spa [] let replicate count (initial : 'T) = ListChecks.replicate (nameof count) count - Array.create count initial |> ofFreshArray + // Array.Fill is vectorized, where Array.create stores the elements one by one: 0.8 to 0.9 times as long for 100 to a + // million strings, and the same time for integers + Buffers.ofFilled count (fun items -> Array.Fill (items, initial, 0, count)) [] let singleton (value : 'T) = ImmutableList.Create<'T> value @@ -618,9 +625,7 @@ let map3 (mapping : 'T1 -> 'T2 -> 'T3 -> 'U) (list1 : IndexedList<'T1>) (list2 : [] let mapi (mapping : int -> 'T -> 'U) (list : IndexedList<'T>) = checkNotNull (nameof list) list - let results = Array.zeroCreate list.Count - Walk.iteri (fun index item -> results[index] <- mapping index item) list - ofFreshArray results + Buffers.ofFilled list.Count (fun results -> Walk.iteri (fun index item -> results[index] <- mapping index item) list) [] let mapi2 (mapping : int -> 'T1 -> 'T2 -> 'U) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) = @@ -698,9 +703,15 @@ let rev (list : IndexedList<'T>) = // A copy reversed in place and a new balanced tree are about 8 times faster than ImmutableList.Reverse at 100 000 // integers (about 3 ms against 23 ms), and the copy is the only extra allocation (ADR 0001, D5); the copy is fresh, // so nobody else sees it change - let array = Snapshot.ofSeq (nameof list) list - Array.Reverse array - ofFreshArray array + checkNotNull (nameof list) list + let count = list.Count + + Buffers.ofFilled + count + (fun items -> + list.CopyTo items + Array.Reverse (items, 0, count) + ) [] let revRange index count (list : IndexedList<'T>) = @@ -902,21 +913,23 @@ let iteri2 (action : int -> 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list [] let mapFold (mapping : 'State -> 'T -> struct ('Result * 'State)) (state : 'State) (list : IndexedList<'T>) = checkNotNull (nameof list) list - let results = Array.zeroCreate list.Count - // Not a Walk loop: for strings, the loop of Walk took 1.1 to 1.4 times as long as this one once it was compiled into the // library, although its machine code differs only in the order of a copy of the struct that the mapping returns, and // the same source in a script ran 0.8 times as long. The loops of iter, fold, mapi and scan took 0.6 to 0.9 times as long. - let mutable state = state - let mutable index = 0 - - for item in list do - let struct (result, next) = mapping state item - results[index] <- result - state <- next - index <- index + 1 - - struct (ofFreshArray results, state) + Buffers.ofFilledWith + list.Count + (fun results -> + let mutable state = state + let mutable index = 0 + + for item in list do + let struct (result, next) = mapping state item + results[index] <- result + state <- next + index <- index + 1 + + state + ) [] let mapFoldBack (mapping : 'T -> 'State -> struct ('Result * 'State)) (list : IndexedList<'T>) (state : 'State) = @@ -1043,10 +1056,12 @@ let reduceBack (reduction : 'T -> 'T -> 'T) (list : IndexedList<'T>) = [] let scan (folder : 'State -> 'T -> 'State) (state : 'State) (list : IndexedList<'T>) = checkNotNull (nameof list) list - let states = Array.zeroCreate (list.Count + 1) - states[0] <- state - Walk.iteri (fun index item -> states[index + 1] <- folder states[index] item) list - ofFreshArray states + Buffers.ofFilled + (list.Count + 1) + (fun states -> + states[0] <- state + Walk.iteri (fun index item -> states[index + 1] <- folder states[index] item) list + ) [] let scanBack (folder : 'T -> 'State -> 'State) (list : IndexedList<'T>) (state : 'State) = diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs b/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs index 0a1e79ae..1a402377 100644 --- a/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs +++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs @@ -1,7 +1,6 @@ [] module FSharp.Collections.Immutable.Tuple.IndexedList -open System.Collections.Immutable open FSharp.Collections.Immutable // The functions that build a tuple per element share their algorithm with the default module and only pass another @@ -40,31 +39,31 @@ let (|Cons|_|) (list : IndexedList<'T>) = tryUncons list [] let allPairs (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) = Arrays.allPairs referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let indexed (list : IndexedList<'T>) = Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let pairwise (list : IndexedList<'T>) = Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let unzip (list : IndexedList<'T1 * 'T2>) = let struct (first, second) = Arrays.unzip (fun (item1, item2) -> struct (item1, item2)) (Snapshot.ofSeq (nameof list) list) - ImmutableList.CreateRange first, ImmutableList.CreateRange second + IndexedList.ofArray first, IndexedList.ofArray second [] let unzip3 (list : IndexedList<'T1 * 'T2 * 'T3>) = let struct (first, second, third) = Arrays.unzip3 (fun (item1, item2, item3) -> struct (item1, item2, item3)) (Snapshot.ofSeq (nameof list) list) - ImmutableList.CreateRange first, ImmutableList.CreateRange second, ImmutableList.CreateRange third + IndexedList.ofArray first, IndexedList.ofArray second, IndexedList.ofArray third [] let zip (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) = @@ -73,7 +72,7 @@ let zip (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) = ListChecks.sameLengths (nameof list1) list1.Count (nameof list2) list2.Count Arrays.zip referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let zip3 (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) (list3 : IndexedList<'T3>) = @@ -87,7 +86,7 @@ let zip3 (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) (list3 : IndexedL (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2) (Snapshot.ofSeq (nameof list3) list3) - |> ImmutableList.CreateRange + |> IndexedList.ofArray // ---- Folding ---- @@ -114,12 +113,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IndexedList [] let countBy (projection : 'T -> 'Key) (list : IndexedList<'T>) = Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + |> IndexedList.ofArray [] let groupBy (projection : 'T -> 'Key) (list : IndexedList<'T>) = - Arrays.groupBy (fun key items -> key, ImmutableList.CreateRange items) projection (Snapshot.ofSeq (nameof list) list) - |> ImmutableList.CreateRange + Arrays.groupBy (fun key items -> key, IndexedList.ofArray items) projection (Snapshot.ofSeq (nameof list) list) + |> IndexedList.ofArray [] let partition predicate (list : IndexedList<'T>) = diff --git a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs index dd070ade..8405f0ce 100644 --- a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs @@ -1021,3 +1021,96 @@ type IndexedListTests () = |> IndexedList.toArray, $"IndexedList.choose must keep the chosen elements of a long list in order (%d{percent} %% chosen)" ) + + /// The tree of a long array is built from a span: every index must find its element, which a tree that is not balanced + /// as the indexer expects would not. + [] + member _.``ofArray of a long array keeps the order and the indices`` () = + let items = Array.init 100_000 (fun index -> index * 7) + let list = IndexedList.ofArray items + + Assert.HasCount (items.Length, list, "IndexedList.ofArray must keep all elements of a long array") + CollectionAssert.AreEqual (items, IndexedList.toArray list, "IndexedList.ofArray must keep the order of a long array") + + let wrong = [| + for index in 0 .. items.Length - 1 do + if list[index] <> items[index] then + index + |] + Assert.IsEmpty (wrong, "Every index of IndexedList.ofArray must give the element at that index") + + [] + member _.``replicate repeats the value for strings and integers`` (count : int) = + CollectionAssert.AreEqual ( + List.replicate count "x" |> List.toArray, + IndexedList.replicate count "x" |> IndexedList.toArray, + "IndexedList.replicate must repeat the string" + ) + + CollectionAssert.AreEqual ( + List.replicate count 7 |> List.toArray, + IndexedList.replicate count 7 |> IndexedList.toArray, + "IndexedList.replicate must repeat the integer" + ) + + /// The temporary array of these functions is a new array below `Buffers.PooledFrom` elements and a rented one above, so + /// the lengths around it take both ways. The strings show that a rented array is cleared as far as it was written. + [] + [] + [] + [] + [] + [] + [] + member _.``the functions that fill an array give the results of List around the length of the rented array`` (length : int) = + let items = Array.init length (fun index -> $"item %d{index}") + let list = IndexedList.ofArray items + let expected = List.ofArray items + let tag (index : int) (item : string) = $"%d{index}:%s{item}" + let step (total : int) (item : string) = total * 31 + item.Length + + CollectionAssert.AreEqual ( + List.init length (fun index -> $"%d{index}!") + |> List.toArray, + IndexedList.init length (fun index -> $"%d{index}!") + |> IndexedList.toArray, + "IndexedList.init must give the strings in order" + ) + + CollectionAssert.AreEqual ( + List.rev expected |> List.toArray, + IndexedList.rev list |> IndexedList.toArray, + "IndexedList.rev must reverse the strings" + ) + + CollectionAssert.AreEqual ( + List.mapi tag expected |> List.toArray, + IndexedList.mapi tag list |> IndexedList.toArray, + "IndexedList.mapi must map the strings in order" + ) + + CollectionAssert.AreEqual ( + List.scan step 7 expected |> List.toArray, + IndexedList.scan step 7 list |> IndexedList.toArray, + "IndexedList.scan must give the states" + ) + + let struct (mapped, final) = + IndexedList.mapFold (fun total item -> struct (tag total item, step total item)) 7 list + let expectedMapped, expectedFinal = + List.mapFold (fun total item -> tag total item, step total item) 7 expected + + CollectionAssert.AreEqual ( + expectedMapped |> List.toArray, + IndexedList.toArray mapped, + "IndexedList.mapFold must map the strings with the state" + ) + + Assert.AreEqual(expectedFinal, final, "IndexedList.mapFold must return the last state") + + if length >= 2 then + CollectionAssert.AreEqual ( + Array.sub items 1 (length - 2), + IndexedList.sub list 1 (length - 2) |> IndexedList.toArray, + "IndexedList.sub must give the strings of the range" + )