From cea72d90dff3fa0abf76c8d4ab3f22595422c4dd Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Wed, 26 Aug 2026 00:06:27 -0500 Subject: [PATCH 1/9] Optimize String.map (netstandard 2.1+) - eliminate intermediate allocations via String.Create + Span --- src/FSharp.Core/string.fs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 800fcdd7826..7d2d2aece54 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -54,19 +54,30 @@ module String = for i = 0 to str.Length - 1 do f.Invoke(i, str.[i]) +#if NETSTANDARD2_1_OR_GREATER + // Cache SpanAction instance to avoid allocations + let private _mapAction = + System.Buffers.SpanAction char))>(fun (result: Span) (struct (str: string, mapping: char -> char)) -> + for i = 0 to result.Length - 1 do + result[i] <- mapping str[i] + ) +#endif + [] let map (mapping: char -> char) (str: string) = if String.IsNullOrEmpty str then String.Empty else +#if NETSTANDARD2_1_OR_GREATER + String.Create(str.Length, struct (str, mapping), _mapAction) +#else let result = str.ToCharArray() - let mutable i = 0 - - for c in result do - result.[i] <- mapping c - i <- i + 1 - + + for i = 0 to result.Length - 1 do + result[i] <- mapping result[i] + String(result) +#endif [] let mapi (mapping: int -> char -> char) (str: string) = From c8eb6b9615265cf82316eef07b5efb6e8e62fc28 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Wed, 26 Aug 2026 00:12:37 -0500 Subject: [PATCH 2/9] Apply the same optimization to String.mapi --- src/FSharp.Core/string.fs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 7d2d2aece54..f005ddcf561 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -79,6 +79,15 @@ module String = String(result) #endif +#if NETSTANDARD2_1_OR_GREATER + // Cache SpanAction instance to avoid allocations + let private _mapiAction = + System.Buffers.SpanAction)>(fun (result: Span) (struct (str, mapping)) -> + for i = 0 to result.Length - 1 do + result[i] <- mapping.Invoke (i, str[i]) + ) +#endif + [] let mapi (mapping: int -> char -> char) (str: string) = let len = length str @@ -86,16 +95,18 @@ module String = if len = 0 then String.Empty else +#if NETSTANDARD2_1_OR_GREATER + let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) + String.Create(len, struct (str, f), _mapiAction) +#else let result = str.ToCharArray() let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) - let mutable i = 0 - - while i < len do - result.[i] <- f.Invoke(i, result.[i]) - i <- i + 1 + for i = 0 to result.Length - 1 do + result[i] <- f.Invoke(i, result[i]) String(result) +#endif [] let filter (predicate: char -> bool) (str: string) = From aa195d9ba3f076641dc1602b51a04d0501ad009b Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 00:27:32 -0500 Subject: [PATCH 3/9] Optimize `String.filter` (netstandard 2.1+) - reduce allocations using stack allocation when possible --- src/FSharp.Core/FSharp.Core.fsproj | 12 ++++++------ src/FSharp.Core/string.fs | 31 ++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/FSharp.Core/FSharp.Core.fsproj b/src/FSharp.Core/FSharp.Core.fsproj index f1777c72b39..65ca6c36899 100644 --- a/src/FSharp.Core/FSharp.Core.fsproj +++ b/src/FSharp.Core/FSharp.Core.fsproj @@ -147,6 +147,12 @@ Collections/seq.fs + + NativeInterop/nativeptr.fsi + + + NativeInterop/nativeptr.fs + Collections/string.fsi @@ -213,12 +219,6 @@ Quotations/quotations.fs - - NativeInterop/nativeptr.fsi - - - NativeInterop/nativeptr.fs - Control/event.fsi diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index f005ddcf561..f3de304baf5 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -8,6 +8,7 @@ open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators open Microsoft.FSharp.Core.Operators open Microsoft.FSharp.Core.Operators.Checked open Microsoft.FSharp.Collections +open Microsoft.FSharp.NativeInterop open Microsoft.FSharp.Primitives.Basics [] @@ -18,7 +19,12 @@ module String = /// and is equal to 80_000 / sizeof [] let LOH_CHAR_THRESHOLD = 40_000 - + +#if NETSTANDARD2_1_OR_GREATER + [] + let STACKALLOC_THRESHOLD = 512 +#endif + [] let length (str: string) = if isNull str then 0 else str.Length @@ -108,8 +114,11 @@ module String = String(result) #endif + // let inline filterBuildString (source: string, target: Span, predicate: char -> bool) = + [] let filter (predicate: char -> bool) (str: string) = + let len = length str if len = 0 then @@ -129,16 +138,30 @@ module String = res.ToString() else - // Must do it this way, since array.fs is not yet in scope, but this is safe - let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + + let target = +#if NETSTANDARD2_1_OR_GREATER +#nowarn "9" + if len <= STACKALLOC_THRESHOLD then + Span((NativePtr.toVoidPtr (NativePtr.stackalloc STACKALLOC_THRESHOLD)), len) + else + Span(Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len) +#warnon "9" +#else + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len +#endif let mutable i = 0 for c in str do if predicate c then target.[i] <- c i <- i + 1 - + +#if NETSTANDARD2_1_OR_GREATER + String(target.Slice(0, i)) +#else String(target, 0, i) +#endif [] let collect (mapping: char -> string) (str: string) = From f3a679ddf0b72ac3c51949fb1f1b33c7e31e2a11 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 00:32:31 -0500 Subject: [PATCH 4/9] Refactor --- src/FSharp.Core/string.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index f3de304baf5..e241c4dba73 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -101,12 +101,11 @@ module String = if len = 0 then String.Empty else -#if NETSTANDARD2_1_OR_GREATER let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) +#if NETSTANDARD2_1_OR_GREATER String.Create(len, struct (str, f), _mapiAction) #else let result = str.ToCharArray() - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) for i = 0 to result.Length - 1 do result[i] <- f.Invoke(i, result[i]) From d9027489a555abb42fd4e7dd6c18db09f1ac8676 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 00:34:32 -0500 Subject: [PATCH 5/9] Add a comment back --- src/FSharp.Core/string.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index e241c4dba73..13cafa537d9 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -144,9 +144,11 @@ module String = if len <= STACKALLOC_THRESHOLD then Span((NativePtr.toVoidPtr (NativePtr.stackalloc STACKALLOC_THRESHOLD)), len) else + // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive. Span(Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len) #warnon "9" #else + // same as above Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len #endif let mutable i = 0 From d4c3528915fee1c945f499e1a9f0dc6939bc9889 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 01:05:08 -0500 Subject: [PATCH 6/9] Optimize `String.replicate` (netstandard 2.1+) - reduce allocations with Span magic --- src/FSharp.Core/string.fs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 13cafa537d9..00b58e8ddb2 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -185,6 +185,27 @@ module String = res.ToString() +#if NETSTANDARD2_1_OR_GREATER + let _replicateAction = + System.Buffers.SpanAction(fun (target: Span) (str: string) -> + let len = str.Length + let source = str.AsSpan() + + // O(log(n)) performance loop: + // Copy first string, then keep copying what we already copied + // (i.e., doubling it) until we reach or pass the halfway point + source.CopyTo(target) + let mutable i = len + + while i * 2 < target.Length do + target.Slice(0, i).CopyTo(target.Slice(i, i)) + i <- i * 2 + + // finally, copy the remaining half, or less-then half + target.Slice(0, target.Length - i).CopyTo(target.Slice(i, target.Length - i)) + ) +#endif + [] let replicate (count: int) (str: string) = if count < 0 then @@ -206,6 +227,9 @@ module String = | _ -> String.Concat(str, str, str, str) else +#if NETSTANDARD2_1_OR_GREATER + String.Create(len * count, str, _replicateAction) +#else // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive. let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len * count) @@ -222,9 +246,10 @@ module String = Array.Copy(target, 0, target, i, i) i <- i * 2 - // finally, copy the remain half, or less-then half + // finally, copy the remaining half, or less-then half Array.Copy(target, 0, target, i, target.Length - i) String(target) +#endif [] let forall predicate (str: string) = From 6c74581a6f9c29ea69d6e8b01b536d2a233b689d Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 21:11:00 -0500 Subject: [PATCH 7/9] Fix a nowarn that breaks fantomas --- src/FSharp.Core/string.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 00b58e8ddb2..4285645a65f 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -11,6 +11,8 @@ open Microsoft.FSharp.Collections open Microsoft.FSharp.NativeInterop open Microsoft.FSharp.Primitives.Basics +#nowarn "9" // Uses of this construct may result in the generation of unverifiable .NET IL code + [] [] module String = @@ -113,8 +115,6 @@ module String = String(result) #endif - // let inline filterBuildString (source: string, target: Span, predicate: char -> bool) = - [] let filter (predicate: char -> bool) (str: string) = @@ -140,13 +140,11 @@ module String = let target = #if NETSTANDARD2_1_OR_GREATER -#nowarn "9" if len <= STACKALLOC_THRESHOLD then Span((NativePtr.toVoidPtr (NativePtr.stackalloc STACKALLOC_THRESHOLD)), len) else // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive. Span(Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len) -#warnon "9" #else // same as above Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len From f555ee684b491e461597bd25f1798aa66ead4649 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 21:11:57 -0500 Subject: [PATCH 8/9] Run fantomas on string.fs --- src/FSharp.Core/string.fs | 47 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 4285645a65f..89b18ff69c2 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -21,12 +21,12 @@ module String = /// and is equal to 80_000 / sizeof [] let LOH_CHAR_THRESHOLD = 40_000 - + #if NETSTANDARD2_1_OR_GREATER [] let STACKALLOC_THRESHOLD = 512 #endif - + [] let length (str: string) = if isNull str then 0 else str.Length @@ -65,12 +65,12 @@ module String = #if NETSTANDARD2_1_OR_GREATER // Cache SpanAction instance to avoid allocations let private _mapAction = - System.Buffers.SpanAction char))>(fun (result: Span) (struct (str: string, mapping: char -> char)) -> - for i = 0 to result.Length - 1 do - result[i] <- mapping str[i] - ) + System.Buffers.SpanAction char))> + (fun (result: Span) (struct (str: string, mapping: char -> char)) -> + for i = 0 to result.Length - 1 do + result[i] <- mapping str[i]) #endif - + [] let map (mapping: char -> char) (str: string) = if String.IsNullOrEmpty str then @@ -80,22 +80,22 @@ module String = String.Create(str.Length, struct (str, mapping), _mapAction) #else let result = str.ToCharArray() - + for i = 0 to result.Length - 1 do result[i] <- mapping result[i] - + String(result) #endif #if NETSTANDARD2_1_OR_GREATER // Cache SpanAction instance to avoid allocations let private _mapiAction = - System.Buffers.SpanAction)>(fun (result: Span) (struct (str, mapping)) -> - for i = 0 to result.Length - 1 do - result[i] <- mapping.Invoke (i, str[i]) - ) + System.Buffers.SpanAction)> + (fun (result: Span) (struct (str, mapping)) -> + for i = 0 to result.Length - 1 do + result[i] <- mapping.Invoke(i, str[i])) #endif - + [] let mapi (mapping: int -> char -> char) (str: string) = let len = length str @@ -117,7 +117,7 @@ module String = [] let filter (predicate: char -> bool) (str: string) = - + let len = length str if len = 0 then @@ -137,7 +137,7 @@ module String = res.ToString() else - + let target = #if NETSTANDARD2_1_OR_GREATER if len <= STACKALLOC_THRESHOLD then @@ -146,8 +146,8 @@ module String = // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive. Span(Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len) #else - // same as above - Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len + // same as above + Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len #endif let mutable i = 0 @@ -155,7 +155,7 @@ module String = if predicate c then target.[i] <- c i <- i + 1 - + #if NETSTANDARD2_1_OR_GREATER String(target.Slice(0, i)) #else @@ -194,16 +194,15 @@ module String = // (i.e., doubling it) until we reach or pass the halfway point source.CopyTo(target) let mutable i = len - + while i * 2 < target.Length do target.Slice(0, i).CopyTo(target.Slice(i, i)) i <- i * 2 - + // finally, copy the remaining half, or less-then half - target.Slice(0, target.Length - i).CopyTo(target.Slice(i, target.Length - i)) - ) + target.Slice(0, target.Length - i).CopyTo(target.Slice(i, target.Length - i))) #endif - + [] let replicate (count: int) (str: string) = if count < 0 then From 5aa9391f43d7e533d9815b4fe9245df4881833f8 Mon Sep 17 00:00:00 2001 From: John Wostenberg Date: Thu, 27 Aug 2026 21:13:35 -0500 Subject: [PATCH 9/9] Correct typo --- src/FSharp.Core/string.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Core/string.fs b/src/FSharp.Core/string.fs index 89b18ff69c2..73052a5c25d 100644 --- a/src/FSharp.Core/string.fs +++ b/src/FSharp.Core/string.fs @@ -243,7 +243,7 @@ module String = Array.Copy(target, 0, target, i, i) i <- i * 2 - // finally, copy the remaining half, or less-then half + // finally, copy the remaining half, or less-than half Array.Copy(target, 0, target, i, target.Length - i) String(target) #endif