Skip to content

Commit 7b29d44

Browse files
authored
Improve perf for String.filter up to 3x (#9509)
* Improve perf for String.filter 2-2.5x * Cleanup: remove "foo" etc in tests * Add tests for new execution path for LOH in String.filter * Change test string
1 parent a166922 commit 7b29d44

File tree

2 files changed

+36
-7
lines changed

2 files changed

+36
-7
lines changed

src/fsharp/FSharp.Core/string.fs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ namespace Microsoft.FSharp.Core
1212
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
1313
[<RequireQualifiedAccess>]
1414
module String =
15+
[<Literal>]
16+
/// LOH threshold is calculated from FSharp.Compiler.AbstractIL.Internal.Library.LOH_SIZE_THRESHOLD_BYTES,
17+
/// and is equal to 80_000 / sizeof<char>
18+
let LOH_CHAR_THRESHOLD = 40_000
19+
1520
[<CompiledName("Length")>]
1621
let length (str:string) = if isNull str then 0 else str.Length
1722

@@ -53,13 +58,30 @@ namespace Microsoft.FSharp.Core
5358

5459
[<CompiledName("Filter")>]
5560
let filter (predicate: char -> bool) (str:string) =
56-
if String.IsNullOrEmpty str then
61+
let len = length str
62+
63+
if len = 0 then
5764
String.Empty
58-
else
59-
let res = StringBuilder str.Length
65+
66+
elif len > LOH_CHAR_THRESHOLD then
67+
// By using SB here, which is twice slower than the optimized path, we prevent LOH allocations
68+
// and 'stop the world' collections if the filtering results in smaller strings.
69+
// We also don't pre-allocate SB here, to allow for less mem pressure when filter result is small.
70+
let res = StringBuilder()
6071
str |> iter (fun c -> if predicate c then res.Append c |> ignore)
6172
res.ToString()
6273

74+
else
75+
// Must do it this way, since array.fs is not yet in scope, but this is safe
76+
let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len
77+
let mutable i = 0
78+
for c in str do
79+
if predicate c then
80+
target.[i] <- c
81+
i <- i + 1
82+
83+
String(target, 0, i)
84+
6385
[<CompiledName("Collect")>]
6486
let collect (mapping: char -> string) (str:string) =
6587
if String.IsNullOrEmpty str then

tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,25 @@ type StringModule() =
8787

8888
[<Test>]
8989
member this.Filter() =
90-
let e1 = String.filter (fun c -> true) "foo"
91-
Assert.AreEqual("foo", e1)
90+
let e1 = String.filter (fun c -> true) "Taradiddle"
91+
Assert.AreEqual("Taradiddle", e1)
9292

9393
let e2 = String.filter (fun c -> true) null
9494
Assert.AreEqual("", e2)
9595

96-
let e3 = String.filter (fun c -> c <> 'o') "foo bar"
97-
Assert.AreEqual("f bar", e3)
96+
let e3 = String.filter Char.IsUpper "How Vexingly Quick Daft Zebras Jump!"
97+
Assert.AreEqual("HVQDZJ", e3)
9898

9999
let e4 = String.filter (fun c -> c <> 'o') ""
100100
Assert.AreEqual("", e4)
101101

102+
let e5 = String.filter (fun c -> c > 'B' ) "ABRACADABRA"
103+
Assert.AreEqual("RCDR", e5)
104+
105+
// LOH test with 55k string, which is 110k bytes
106+
let e5 = String.filter (fun c -> c > 'B' ) (String.replicate 5_000 "ABRACADABRA")
107+
Assert.AreEqual(String.replicate 5_000 "RCDR", e5)
108+
102109
[<Test>]
103110
member this.Collect() =
104111
let e1 = String.collect (fun c -> "a"+string c) "foo"

0 commit comments

Comments
 (0)