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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<Compile Include="Helper\ListArrays.fs" />
<Compile Include="Helper\Snapshot.fs" />
<Compile Include="Helper\Collectors.fs" />
<Compile Include="Helper\Walk.fs" />
<Compile Include="Helper\Buffers.fs" />
<Compile Include="Helper\Lengths.fs" />
<Compile Include="Helper\DescendingSorts.fs" />
<Compile Include="Helper\ListRanges.fs" />
Expand Down
125 changes: 125 additions & 0 deletions src/FSharp.Collections.Immutable/Helper/Buffers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/// <summary>
/// The buffer in which a function collects the elements that it chooses one by one, until it builds the tree of the result
/// (ADR 0001, D5).
/// </summary>
/// <remarks>
/// <para>
/// A result of unknown length waits in a <see cref="T:System.Collections.Generic.List`1"/> that doubles its array as it
/// fills, and the tree is built once, from its elements. The growth costs the copies, and for a result of some 20 000
/// integers or 10 000 references or more the arrays of the growth are allocated in the large object heap, which a collection
/// of the second generation reclaims; for a million elements the list allocated 18 % (integers) to 35 % (strings) more than
/// the tree needs.
/// </para>
/// <para>
/// The number of elements to choose from is known, and it bounds the result. The buffer takes an array of that length: a
/// new one for a short result, where the array is cheap and the pool would only add its own work, and one rented from
/// <see cref="P:System.Buffers.ArrayPool`1.Shared"/> for a long one, which allocates nothing in the steady state. An array
/// of the length of the source is more than the result needs when few of the elements are chosen, and it would be the
/// array of a hundred million elements for a list of that length. The rented array is therefore at most
/// <see cref="F:FSharp.Collections.Immutable.Buffers.PooledUpTo"/> elements long; a longer result moves into a list.
/// </para>
/// <para>
/// A buffer that is not turned into a list, because the chooser threw, is not returned to the pool. The pool allocates
/// another array, and nothing leaks.
/// </para>
/// </remarks>
module internal FSharp.Collections.Immutable.Buffers

open System
open System.Buffers
open System.Collections.Generic
open System.Collections.Immutable
open System.Runtime.CompilerServices
open System.Runtime.InteropServices

/// <summary>
/// The least length of the source for which the buffer is rented from the pool.
/// </summary>
/// <remarks>
/// For 4 to 2048 elements, an array of its own and a rented one took the same time, 0 to 20 % less than the list when all
/// of the elements were chosen; the first call that rents an array of a length allocates it, and the first call took 6 to
/// 81 µs for 4 to 2048 elements.
/// An array below the limit is a small allocation that the next collection of the youngest generation reclaims.
/// </remarks>
[<Literal>]
let PooledFrom = 1024

/// <summary>
/// The most elements in the array that the buffer rents; a result that is longer moves into a list.
/// </summary>
[<Literal>]
let PooledUpTo = 1048576

/// <summary>
/// Collects the elements that a function chooses from a source of a known length, and builds the indexed list of them.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <remarks>
/// A buffer is used once: <see cref="M:FSharp.Collections.Immutable.Buffers.ChosenBuffer`1.ToImmutable"/> ends it.
/// </remarks>
[<Sealed>]
type ChosenBuffer<'T> (bound : int) =

let pooled = bound >= PooledFrom

let mutable buffer : 'T array =
if bound = 0 then
Array.Empty<'T>()
elif pooled then
ArrayPool<'T>.Shared.Rent(min bound PooledUpTo)
else
Array.zeroCreate bound

let mutable count = 0
let mutable overflow : ResizeArray<'T> | null = null

/// Returns the rented array to the pool, cleared as far as it was written when it holds references. The buffer is
/// left with an empty array, which is full at once, so that an element goes to the slow path of Add.
member private _.Release () =
if pooled && buffer.Length > 0 then
if RuntimeHelpers.IsReferenceOrContainsReferences<'T>() then
Array.Clear (buffer, 0, count)

ArrayPool<'T>.Shared.Return(buffer, false)

buffer <- Array.Empty<'T>()
count <- 0

/// Moves the elements of the full array into a list that grows, and gives the array back.
member private this.Spill () : ResizeArray<'T> =
let spilled = ResizeArray<'T>(buffer.Length * 2)
CollectionsMarshal.SetCount (spilled, count)
ReadOnlySpan<'T>(buffer, 0, count).CopyTo(CollectionsMarshal.AsSpan spilled)
this.Release ()
overflow <- spilled
spilled

[<MethodImpl(MethodImplOptions.NoInlining)>]
member private this.AddSlow (value : 'T) =
match overflow with
| null -> this.Spill().Add value
| spilled -> spilled.Add value

/// <summary>
/// Appends an element.
/// </summary>
/// <param name="value">The element.</param>
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
member this.Add (value : 'T) =
if count = buffer.Length then
this.AddSlow value
else
buffer[count] <- value
count <- count + 1

/// <summary>
/// The indexed list of the elements that were added, in their order.
/// </summary>
/// <returns>The indexed list, which is empty when nothing was added.</returns>
member this.ToImmutable () : ImmutableList<'T> =
match overflow with
| null ->
let result = ImmutableList.Create<'T>(ReadOnlySpan<'T>(buffer, 0, count))
this.Release ()
result
| spilled -> ImmutableList.Create<'T>(Span<'T>.op_Implicit(CollectionsMarshal.AsSpan spilled))
100 changes: 100 additions & 0 deletions src/FSharp.Collections.Immutable/Helper/Walk.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/// <summary>
/// The loops over the elements of an <see cref="T:System.Collections.Immutable.ImmutableList`1"/>, which walk it with the
/// enumerator that is the faster one for the type of the elements (ADR 0001, D5).
/// </summary>
/// <remarks>
/// <para>
/// <c>ImmutableList&lt;T&gt;.GetEnumerator</c> returns a struct. For a value type that is the fastest way to walk the list: the
/// compiler knows the struct, and its calls are direct and inlined. For a reference type the code of a generic function is
/// shared by all reference types, and each call of a member of a generic struct in it needs a lookup of the exact type
/// at run time. Walking the list through <see cref="T:System.Collections.Generic.IEnumerable`1"/> instead, with a boxed
/// enumerator and calls through the interface, took 0.56 to 0.70 times as long for strings (6 against 10 ns for an
/// element) and 1.1 to 1.5 times as long for integers. As functions of the library, iter and fold took 0.6 to 0.7 times
/// as long for strings of 100 to 100 000 elements, tryPick 0.6, mapi and scan 0.7 to 0.9, and none of them changed for
/// integers. The loop of mapFold stays as it was, because the same loop took 1.1 to 1.4 times as long there.
/// </para>
/// <para>
/// The test <c>typeof&lt;'T&gt;.IsValueType</c> is a constant for the compiler of the run time, so each instantiation of a
/// function keeps one of the two loops. The functions are inline, and the body of a loop is passed as a lambda that the
/// compiler inlines into both loops, so that a body is written once. A body cannot capture a mutable local, which is why
/// <see cref="M:FSharp.Collections.Immutable.Walk.iteri``1"/> and
/// <see cref="M:FSharp.Collections.Immutable.Walk.fold``2"/> count and carry the state themselves.
/// </para>
/// </remarks>
module internal FSharp.Collections.Immutable.Walk

open System.Collections.Immutable

/// <summary>
/// Calls an action for each element of a list, in order.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="list">The list, which is not <see langword="null"/>.</param>
let inline iter ([<InlineIfLambda>] action : 'T -> unit) (list : ImmutableList<'T>) =
if typeof<'T>.IsValueType then
for item in list do
action item
else
for item in (list :> seq<'T>) do
action item

/// <summary>
/// Calls an action for each element of a list, in order, with the index of the element.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="list">The list, which is not <see langword="null"/>.</param>
let inline iteri ([<InlineIfLambda>] action : int -> 'T -> unit) (list : ImmutableList<'T>) =
let mutable index = 0

if typeof<'T>.IsValueType then
for item in list do
action index item
index <- index + 1
else
for item in (list :> seq<'T>) do
action index item
index <- index + 1

/// <summary>
/// Threads a state through the elements of a list, in order.
/// </summary>
/// <param name="folder">The function that returns the next state for the state and the element.</param>
/// <param name="state">The first state.</param>
/// <param name="list">The list, which is not <see langword="null"/>.</param>
/// <returns>The last state.</returns>
let inline fold ([<InlineIfLambda>] folder : 'State -> 'T -> 'State) (state : 'State) (list : ImmutableList<'T>) =
let mutable state = state

if typeof<'T>.IsValueType then
for item in list do
state <- folder state item
else
for item in (list :> seq<'T>) do
state <- folder state item

state

/// <summary>
/// The first value that a chooser returns for the elements of a list, in order, without a call for the elements after it.
/// </summary>
/// <param name="chooser">The chooser.</param>
/// <param name="list">The list, which is not <see langword="null"/>.</param>
/// <returns>The first value, or <see langword="ValueNone"/>.</returns>
let inline tryPick ([<InlineIfLambda>] chooser : 'T -> 'U voption) (list : ImmutableList<'T>) =
let mutable result = ValueNone

if typeof<'T>.IsValueType then
let mutable enumerator = list.GetEnumerator ()

try
while result.IsNone && enumerator.MoveNext () do
result <- chooser enumerator.Current
finally
enumerator.Dispose ()
else
use enumerator = (list :> seq<'T>).GetEnumerator()

while result.IsNone && enumerator.MoveNext () do
result <- chooser enumerator.Current

result
8 changes: 3 additions & 5 deletions src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -370,17 +370,15 @@ let append (list1 : IIndexedList<'T>) (list2 : IIndexedList<'T>) =
[<CompiledName "Choose">]
let choose (chooser : 'T -> 'U voption) (list : IIndexedList<'T>) =
checkNotNull (nameof list) list
// The number of chosen elements is not known in advance, so they wait in a ResizeArray and CreateRange builds the
// balanced tree once. A builder rebalances its tree after every Add and took 2.3 times as long for 100 000
// elements; there is no tree of an input to join to its tree here, as collect and concat have (see Collectors).
let chosen = ResizeArray<'U>()
// As IndexedList.choose: the chosen elements wait in an array as long as the list (see Buffers)
let chosen = Buffers.ChosenBuffer<'U> list.Count

for item in list do
match chooser item with
| ValueSome value -> chosen.Add value
| ValueNone -> ()

ImmutableList.CreateRange chosen
chosen.ToImmutable ()

[<CompiledName "ChunkBySize">]
let chunkBySize chunkSize (list : IIndexedList<'T>) =
Expand Down
Loading