Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/FSharp.Collections.Immutable/Helper/Buffers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,57 @@ type ChosenBuffer<'T> (bound : int) =
this.Release ()
result
| spilled -> ImmutableList.Create<'T>(Span<'T>.op_Implicit(CollectionsMarshal.AsSpan spilled))

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <typeparam name="R">The type of what the function returns.</typeparam>
/// <param name="length">The number of elements.</param>
/// <param name="fill">
/// Writes the elements at the indexes 0 to <paramref name="length"/> - 1 of the array that it gets, which can be longer.
/// </param>
/// <returns>The indexed list, and the value that <paramref name="fill"/> returned.</returns>
/// <remarks>
/// <para>
/// The array is a new one below <see cref="F:FSharp.Collections.Immutable.Buffers.PooledFrom"/> elements, and one rented
/// from the pool above, which is cleared as far as it was written when it holds references and returned.
/// </para>
/// <para>
/// A function that throws does not return the rented array. The pool allocates another one, and nothing leaks.
/// </para>
/// </remarks>
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)

/// <summary>
/// Builds the indexed list of the elements that a function writes into an array of a known length.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="length">The number of elements.</param>
/// <param name="fill">
/// Writes the elements at the indexes 0 to <paramref name="length"/> - 1 of the array that it gets, which can be longer.
/// </param>
/// <returns>The indexed list.</returns>
let ofFilled (length : int) (fill : 'T array -> unit) : ImmutableList<'T> =
let struct (result, ()) = ofFilledWith length fill
result
21 changes: 10 additions & 11 deletions src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[<RequireQualifiedAccess>]
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
Expand Down Expand Up @@ -29,31 +28,31 @@ let (|Cons|_|) (list : IIndexedList<'T>) = tryUncons list
[<CompiledName "AllPairs">]
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

[<CompiledName "Indexed">]
let indexed (list : IIndexedList<'T>) =
Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "Pairwise">]
let pairwise (list : IIndexedList<'T>) =
Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "Unzip">]
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

[<CompiledName "Unzip3">]
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

[<CompiledName "Zip">]
let zip (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) =
Expand All @@ -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

[<CompiledName "Zip3">]
let zip3 (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) (list3 : IIndexedList<'T3>) =
Expand All @@ -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 ----

Expand All @@ -103,12 +102,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IIndexedLis
[<CompiledName "CountBy">]
let countBy (projection : 'T -> 'Key) (list : IIndexedList<'T>) =
Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "GroupBy">]
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

[<CompiledName "Partition">]
let partition predicate (list : IIndexedList<'T>) =
Expand Down
75 changes: 45 additions & 30 deletions src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
)

[<CompiledName "OfArray">]
let ofArray (array : 'T array) =
checkNotNull (nameof array) array
ImmutableList.CreateRange array
ofFreshArray array

[<CompiledName "OfList">]
let ofList (list : 'T list) =
Expand Down Expand Up @@ -90,7 +95,9 @@ let ofSpan (span : Span<'T>) = ImmutableList.Create<'T>(Span<'T>.op_Implicit spa
[<CompiledName "Replicate">]
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))

[<CompiledName "Singleton">]
let singleton (value : 'T) = ImmutableList.Create<'T> value
Expand Down Expand Up @@ -618,9 +625,7 @@ let map3 (mapping : 'T1 -> 'T2 -> 'T3 -> 'U) (list1 : IndexedList<'T1>) (list2 :
[<CompiledName "MapIndexed">]
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)

[<CompiledName "MapIndexed2">]
let mapi2 (mapping : int -> 'T1 -> 'T2 -> 'U) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
Expand Down Expand Up @@ -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)
)

[<CompiledName "ReverseRange">]
let revRange index count (list : IndexedList<'T>) =
Expand Down Expand Up @@ -902,21 +913,23 @@ let iteri2 (action : int -> 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list
[<CompiledName "MapFold">]
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
)

[<CompiledName "MapFoldBack">]
let mapFoldBack (mapping : 'T -> 'State -> struct ('Result * 'State)) (list : IndexedList<'T>) (state : 'State) =
Expand Down Expand Up @@ -1043,10 +1056,12 @@ let reduceBack (reduction : 'T -> 'T -> 'T) (list : IndexedList<'T>) =
[<CompiledName "Scan">]
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
)

[<CompiledName "ScanBack">]
let scanBack (folder : 'T -> 'State -> 'State) (list : IndexedList<'T>) (state : 'State) =
Expand Down
21 changes: 10 additions & 11 deletions src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[<RequireQualifiedAccess>]
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
Expand Down Expand Up @@ -40,31 +39,31 @@ let (|Cons|_|) (list : IndexedList<'T>) = tryUncons list
[<CompiledName "AllPairs">]
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

[<CompiledName "Indexed">]
let indexed (list : IndexedList<'T>) =
Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "Pairwise">]
let pairwise (list : IndexedList<'T>) =
Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "Unzip">]
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

[<CompiledName "Unzip3">]
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

[<CompiledName "Zip">]
let zip (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
Expand All @@ -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

[<CompiledName "Zip3">]
let zip3 (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) (list3 : IndexedList<'T3>) =
Expand All @@ -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 ----

Expand All @@ -114,12 +113,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IndexedList
[<CompiledName "CountBy">]
let countBy (projection : 'T -> 'Key) (list : IndexedList<'T>) =
Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list)
|> ImmutableList.CreateRange
|> IndexedList.ofArray

[<CompiledName "GroupBy">]
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

[<CompiledName "Partition">]
let partition predicate (list : IndexedList<'T>) =
Expand Down
Loading