From df91e9ebb76fbe517539a84753b1db7cbfae59f4 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Wed, 21 Oct 2026 04:30:47 +0200 Subject: [PATCH] perf: `SortedSet.exists` and `forall` walk a set of a reference type through the interface The code that all reference types share needs a lookup of the exact type at run time for each call of a member of a generic struct, and the struct enumerator of `ImmutableSortedSet` is made of such calls. A walk through `IEnumerable` took 0.53 to 0.58 times as long for strings of 100 to 100 000 elements (13 against 23 ns for an element) and 1.3 to 1.7 times as long for integers, so `typeof<'T>.IsValueType` picks the enumerator. Compiled into the library and measured against a copy of the old loop in the same process, `exists` and `forall` of strings took 0.47 to 0.57 times as long for 100 to 100 000 elements, and the same time for integers (0.94 to 1.03). The cost is the boxed enumerator, 64 bytes per call, which the comment of the functions already counted for `Seq.exists`. The contracts run on integers, so strings get tests of their own: the results of `Set` for the empty set and for the first, a middle, the last and an absent element, and the stop at the first match and at the first failure. Co-Authored-By: Claude Sonnet 5.5 --- .../SortedSet/SortedSet.fs | 47 +++++++---- .../SortedSet/SortedSetTests.fs | 81 +++++++++++++++++++ 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs index a4f57a9e..8c58270e 100644 --- a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs @@ -146,40 +146,55 @@ let contains (element : 'T) (set : SortedSet<'T>) = checkNotNull (nameof set) set set.Contains element -// exists and forall loop over the struct enumerator of the sorted set, which allocates nothing. LINQ Any and All allocated -// 152 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 64 bytes, the boxed enumerator -// (measured in Release; a measured faster path replaces a higher priority, D5). Dispose returns the pooled stack of the -// enumerator. +// exists and forall loop over the struct enumerator of the sorted set for a value type, which allocates nothing. LINQ Any and +// All allocated 152 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 64 bytes, the boxed +// enumerator (measured in Release; a measured faster path replaces a higher priority, D5). A set of a reference type is walked +// through the interface instead, at the price of that boxed enumerator: the code that all reference types share needs a +// lookup of the exact type at run time for each call of a member of the generic struct enumerator, and for strings of 100 to +// 100 000 elements exists and forall took 0.47 to 0.57 times as long, where for integers the interface took 1.3 to 1.7 times +// as long. Dispose returns the pooled stack of the enumerator. [] let exists (predicate : 'T -> bool) (set : SortedSet<'T>) = checkNotNull (nameof set) set - let mutable enumerator = set.GetEnumerator () + let mutable found = false - try - let mutable found = false + if typeof<'T>.IsValueType then + let mutable enumerator = set.GetEnumerator () + + try + while not found && enumerator.MoveNext () do + found <- predicate enumerator.Current + finally + enumerator.Dispose () + else + use enumerator = (set :> seq<'T>).GetEnumerator() while not found && enumerator.MoveNext () do found <- predicate enumerator.Current - found - finally - enumerator.Dispose () + found [] let forall (predicate : 'T -> bool) (set : SortedSet<'T>) = checkNotNull (nameof set) set - let mutable enumerator = set.GetEnumerator () + let mutable satisfied = true - try - let mutable satisfied = true + if typeof<'T>.IsValueType then + let mutable enumerator = set.GetEnumerator () + + try + while satisfied && enumerator.MoveNext () do + satisfied <- predicate enumerator.Current + finally + enumerator.Dispose () + else + use enumerator = (set :> seq<'T>).GetEnumerator() while satisfied && enumerator.MoveNext () do satisfied <- predicate enumerator.Current - satisfied - finally - enumerator.Dispose () + satisfied [] let isProperSubset (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs index f7752e2c..c9e23a08 100644 --- a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs @@ -1071,6 +1071,87 @@ type SortedSetTests () = "SortedSet.maxElement of an empty sorted set must throw" ) + // ---- Search ---- + + // exists and forall walk a set of a reference type through the interface and a set of a value type through the struct + // enumerator, and the contracts run on integers, so strings have tests of their own + + [] + member _.``exists and forall of strings give the results of Set`` (count : int, position : string) = + let items = Array.init count (fun index -> "x" + index.ToString ()) + + let target = + match position with + | "first" -> "x0" + | "middle" -> "x" + (count / 2).ToString() + | "last" -> "x" + (count - 1).ToString() + | _ -> "absent" + + let set = SortedSet.ofArray items + let expected = Set.ofArray items + + Assert.AreEqual ( + Set.exists ((=) target) expected, + SortedSet.exists ((=) target) set, + "SortedSet.exists must find the element as Set.exists does" + ) + + Assert.AreEqual ( + Set.forall ((<>) target) expected, + SortedSet.forall ((<>) target) set, + "SortedSet.forall must reject the element as Set.forall does" + ) + + Assert.AreEqual ( + Set.exists (fun (text : string) -> text.Length > 0) expected, + SortedSet.exists (fun (text : string) -> text.Length > 0) set, + "SortedSet.exists must answer for a predicate that fits some elements as Set.exists does" + ) + + Assert.AreEqual ( + Set.forall (fun (text : string) -> text.Length > 0) expected, + SortedSet.forall (fun (text : string) -> text.Length > 0) set, + "SortedSet.forall must answer for a predicate that fits all elements as Set.forall does" + ) + + [] + member _.``exists of strings stops at the first match`` () = + let calls = ref 0 + + let found = + SortedSet.exists + (fun word -> + calls.Value <- calls.Value + 1 + word = "A" + ) + (SortedSet.ofArray words) + + Assert.IsTrue (found, "SortedSet.exists must find the first string in the order") + Assert.AreEqual (1, calls.Value, "SortedSet.exists must not call the predicate after the first match") + + [] + member _.``forall of strings stops at the first element that fails`` () = + let calls = ref 0 + + let holds = + SortedSet.forall + (fun word -> + calls.Value <- calls.Value + 1 + word <> "A" + ) + (SortedSet.ofArray words) + + Assert.IsFalse (holds, "SortedSet.forall must fail on the first string in the order") + Assert.AreEqual (1, calls.Value, "SortedSet.forall must not call the predicate after the first failure") + // ---- Null inputs ---- []