Skip to content

Commit 1c5ce38

Browse files
author
Paul Westcott
committed
Split Seq.groupBy for ValueType/RefType
The StructBox makes code that contains "hard" tail calls, which means that performance suffers under the 64 bit JIT
1 parent d02a425 commit 1c5ce38

1 file changed

Lines changed: 35 additions & 22 deletions

File tree

‎src/fsharp/FSharp.Core/seq.fs‎

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,32 +1444,45 @@ namespace Microsoft.FSharp.Collections
14441444
checkNonNull "source" source
14451445
mkSeq (fun () -> source.GetEnumerator())
14461446

1447-
1448-
14491447
[<CompiledName("GroupBy")>]
1450-
let groupBy keyf seq =
1448+
let inline groupByImpl (comparer:IEqualityComparer<'SafeKey>) (keyf:'T->'SafeKey) (getKey:'SafeKey->'Key) (seq:seq<'T>) =
1449+
let dict = Dictionary<_,ResizeArray<_>> comparer
1450+
1451+
// Previously this was 1, but I think this is rather stingy, considering that we are alreadying paying
1452+
// for at least a key, the ResizeArray reference, which includes an array reference, an Entry in the
1453+
// Dictionary, plus any empty space in the Dictionary of unfilled hash buckets.
1454+
let minimumBucketSize = 4
1455+
1456+
// Build the groupings
1457+
seq |> iter (fun v ->
1458+
let key = keyf v
1459+
let mutable prev = Unchecked.defaultof<_>
1460+
match dict.TryGetValue (key, &prev) with
1461+
| true -> prev.Add v
1462+
| false ->
1463+
let prev = ResizeArray minimumBucketSize
1464+
dict.[key] <- prev
1465+
prev.Add v)
1466+
1467+
// Trim the size of each result group, don't trim very small buckets, as excessive work, and garbage for
1468+
// minimal gain
1469+
dict |> iter (fun group -> if group.Value.Count > minimumBucketSize then group.Value.TrimExcess())
1470+
1471+
// Return the sequence-of-sequences. Don't reveal the
1472+
// internal collections: just reveal them as sequences
1473+
dict |> map (fun group -> (getKey group.Key, readonly group.Value))
14511474

1452-
mkDelayedSeq (fun () ->
1453-
// Wrap a StructBox(_) around all keys in case the key type is itself a type using null as a representation
1454-
let dict = new Dictionary<StructBox<'Key>,ResizeArray<'T>>(StructBox<'Key>.Comparer)
1475+
// We avoid wrapping a StructBox, because under 64 JIT we get some "hard" tailcalls which affect performance
1476+
let groupByValueType (keyf:'T->'Key) (seq:seq<'T>) = seq |> groupByImpl HashIdentity.Structural<'Key> keyf id
14551477

1456-
// Build the groupings
1457-
seq |> iter (fun v ->
1458-
let key = StructBox (keyf v)
1459-
let ok,prev = dict.TryGetValue(key)
1460-
if ok then
1461-
prev.Add(v)
1462-
else
1463-
let prev = new ResizeArray<'T>(1)
1464-
dict.[key] <- prev
1465-
prev.Add(v))
1478+
// Wrap a StructBox around all keys in case the key type is itself a type using null as a representation
1479+
let groupByRefType (keyf:'T->'Key) (seq:seq<'T>) = seq |> groupByImpl StructBox<'Key>.Comparer (fun t -> StructBox (keyf t)) (fun sb -> sb.Value)
14661480

1467-
// Trim the size of each result group.
1468-
dict |> iter (fun group -> group.Value.TrimExcess())
1469-
1470-
// Return the sequence-of-sequences. Don't reveal the
1471-
// internal collections: just reveal them as sequences
1472-
dict |> map (fun group -> (group.Key.Value, readonly group.Value)))
1481+
[<CompiledName("GroupBy")>]
1482+
let groupBy (keyf:'T->'Key) (seq:seq<'T>) =
1483+
if typeof<'T>.IsValueType
1484+
then mkDelayedSeq (fun () -> groupByValueType keyf seq)
1485+
else mkDelayedSeq (fun () -> groupByRefType keyf seq)
14731486

14741487
[<CompiledName("Distinct")>]
14751488
let distinct source =

0 commit comments

Comments
 (0)