@@ -1932,7 +1932,9 @@ module Array =
1932
1932
result
1933
1933
1934
1934
module Parallel =
1935
+ open System.Threading
1935
1936
open System.Threading .Tasks
1937
+ open System.Collections .Concurrent
1936
1938
1937
1939
[<CompiledName( " TryFindIndex" ) >]
1938
1940
let tryFindIndex predicate ( array : _ []) =
@@ -2054,6 +2056,126 @@ module Array =
2054
2056
2055
2057
result
2056
2058
2059
+ // The following two parameters were benchmarked and found to be optimal.
2060
+ // Benchmark was run using: 11th Gen Intel Core i9-11950H 2.60GHz, 1 CPU, 16 logical and 8 physical cores
2061
+ let private maxPartitions = Environment.ProcessorCount // The maximum number of partitions to use
2062
+ let private minChunkSize = 256 // The minimum size of a chunk to be sorted in parallel
2063
+
2064
+ let private createPartitionsUpTo maxIdxExclusive ( array : 'T []) =
2065
+ [|
2066
+ let chunkSize =
2067
+ match maxIdxExclusive with
2068
+ | smallSize when smallSize < minChunkSize -> smallSize
2069
+ | biggerSize when biggerSize % maxPartitions = 0 -> biggerSize / maxPartitions
2070
+ | biggerSize -> ( biggerSize / maxPartitions) + 1
2071
+
2072
+ let mutable offset = 0
2073
+
2074
+ while ( offset + chunkSize) < maxIdxExclusive do
2075
+ yield new ArraySegment< 'T>( array, offset, chunkSize)
2076
+ offset <- offset + chunkSize
2077
+
2078
+ yield new ArraySegment< 'T>( array, offset, maxIdxExclusive - offset)
2079
+ |]
2080
+
2081
+ let inline groupByImplParallel
2082
+ ( comparer : IEqualityComparer < 'SafeKey >)
2083
+ ( [<InlineIfLambda>] keyf : 'T -> 'SafeKey )
2084
+ ( [<InlineIfLambda>] getKey : 'SafeKey -> 'Key )
2085
+ ( array : 'T [])
2086
+ =
2087
+ let counts =
2088
+ new ConcurrentDictionary<_, _>(
2089
+ concurrencyLevel = maxPartitions,
2090
+ capacity = Operators.min ( array.Length) 1_000 ,
2091
+ comparer = comparer
2092
+ )
2093
+
2094
+ let valueFactory = new Func<_, _>( fun _ -> ref 0 )
2095
+
2096
+ let projectedValues =
2097
+ Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked array.Length
2098
+
2099
+ let inputChunks = createPartitionsUpTo array.Length array
2100
+
2101
+ Parallel.For(
2102
+ 0 ,
2103
+ inputChunks.Length,
2104
+ fun chunkIdx ->
2105
+ let chunk = inputChunks[ chunkIdx]
2106
+
2107
+ for elemIdx = chunk.Offset to ( chunk.Offset + chunk.Count - 1 ) do
2108
+ let projected = keyf array[ elemIdx]
2109
+ projectedValues[ elemIdx] <- projected
2110
+ let counter = counts.GetOrAdd( projected, valueFactory = valueFactory)
2111
+ Interlocked.Increment( counter) |> ignore
2112
+ )
2113
+ |> ignore
2114
+
2115
+ let finalResults =
2116
+ Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked counts.Count
2117
+
2118
+ let mutable finalIdx = 0
2119
+
2120
+ let finalResultsLookup =
2121
+ new Dictionary< 'SafeKey, int ref * 'T[]>( capacity = counts.Count, comparer = comparer)
2122
+
2123
+ for kvp in counts do
2124
+ let arrayForThisGroup =
2125
+ Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked kvp.Value.Value
2126
+
2127
+ finalResults.[ finalIdx] <- getKey kvp.Key, arrayForThisGroup
2128
+ finalResultsLookup[ kvp.Key] <- kvp.Value, arrayForThisGroup
2129
+ finalIdx <- finalIdx + 1
2130
+
2131
+ Parallel.For(
2132
+ 0 ,
2133
+ inputChunks.Length,
2134
+ fun chunkIdx ->
2135
+ let chunk = inputChunks[ chunkIdx]
2136
+
2137
+ for elemIdx = chunk.Offset to ( chunk.Offset + chunk.Count - 1 ) do
2138
+ let key = projectedValues[ elemIdx]
2139
+ let ( counter , arrayForThisGroup ) = finalResultsLookup[ key]
2140
+ let idxToWrite = Interlocked.Decrement( counter)
2141
+ arrayForThisGroup[ idxToWrite] <- array[ elemIdx]
2142
+ )
2143
+ |> ignore
2144
+
2145
+ finalResults
2146
+
2147
+ let groupByValueTypeParallel ( keyf : 'T -> 'Key ) ( array : 'T []) =
2148
+ // Is it a bad idea to put floating points as keys for grouping? Yes
2149
+ // But would the implementation fail with KeyNotFound "nan" if we just leave it? Also yes
2150
+ // Here we enforce nan=nan equality to prevent throwing
2151
+ if typeof< 'Key> = typeof< float> || typeof< 'Key> = typeof< float32> then
2152
+ let genericCmp =
2153
+ HashIdentity.FromFunctions< 'Key>
2154
+ ( LanguagePrimitives.GenericHash)
2155
+ ( LanguagePrimitives.GenericEqualityER)
2156
+
2157
+ groupByImplParallel genericCmp keyf id array
2158
+ else
2159
+ groupByImplParallel HashIdentity.Structural< 'Key> keyf id array
2160
+
2161
+ // Just like in regular Array.groupBy: Wrap a StructBox around all keys in order to avoid nulls
2162
+ // (dotnet doesn't allow null keys in dictionaries)
2163
+ let groupByRefTypeParallel ( keyf : 'T -> 'Key ) ( array : 'T []) =
2164
+ groupByImplParallel
2165
+ RuntimeHelpers.StructBox< 'Key>. Comparer
2166
+ ( fun t -> RuntimeHelpers.StructBox( keyf t))
2167
+ ( fun sb -> sb.Value)
2168
+ array
2169
+
2170
+ [<CompiledName( " GroupBy" ) >]
2171
+ let groupBy ( projection : 'T -> 'Key ) ( array : 'T []) =
2172
+ checkNonNull " array" array
2173
+
2174
+ if typeof< 'Key>. IsValueType then
2175
+ groupByValueTypeParallel projection array
2176
+ else
2177
+ groupByRefTypeParallel projection array
2178
+
2057
2179
[<CompiledName( " Iterate" ) >]
2058
2180
let iter action ( array : 'T []) =
2059
2181
checkNonNull " array" array
0 commit comments