diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 231cca712e2..44065c7c818 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -3,9 +3,9 @@
-
+
https://github.com/dotnet/arcade
- 9b71be0663493cd0e111b55536a2e1eeb272f54c
+ ed69753a3ffbdaa08365252c710d57a64d17f859
diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config
index 256ffbfb93a..968b39bef5f 100644
--- a/eng/common/sdl/packages.config
+++ b/eng/common/sdl/packages.config
@@ -1,4 +1,4 @@
-
+
diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml
index 52e2ff021d7..bf09d2511c6 100644
--- a/eng/common/templates/job/execute-sdl.yml
+++ b/eng/common/templates/job/execute-sdl.yml
@@ -65,7 +65,7 @@ jobs:
continueOnError: ${{ parameters.sdlContinueOnError }}
- ${{ if eq(parameters.overrideParameters, '') }}:
- powershell: eng/common/sdl/execute-all-sdl-tools.ps1
- -GuardianPackageName Microsoft.Guardian.Cli.0.7.2
+ -GuardianPackageName Microsoft.Guardian.Cli.win10-x64.0.20.1
-NugetPackageDirectory $(Build.SourcesDirectory)\.packages
-AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw)
${{ parameters.additionalParameters }}
diff --git a/global.json b/global.json
index 146979f9b93..7afbdb52d28 100644
--- a/global.json
+++ b/global.json
@@ -9,7 +9,7 @@
}
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20302.3",
+ "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20326.2",
"Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19069.2"
}
}
diff --git a/src/fsharp/FSharp.Core/string.fs b/src/fsharp/FSharp.Core/string.fs
index 7e4b65f1a08..d009615e6aa 100644
--- a/src/fsharp/FSharp.Core/string.fs
+++ b/src/fsharp/FSharp.Core/string.fs
@@ -12,6 +12,11 @@ namespace Microsoft.FSharp.Core
[]
[]
module String =
+ []
+ /// LOH threshold is calculated from FSharp.Compiler.AbstractIL.Internal.Library.LOH_SIZE_THRESHOLD_BYTES,
+ /// and is equal to 80_000 / sizeof
+ let LOH_CHAR_THRESHOLD = 40_000
+
[]
let length (str:string) = if isNull str then 0 else str.Length
@@ -37,9 +42,13 @@ namespace Microsoft.FSharp.Core
if String.IsNullOrEmpty str then
String.Empty
else
- let res = StringBuilder str.Length
- str |> iter (fun c -> res.Append(mapping c) |> ignore)
- res.ToString()
+ let result = str.ToCharArray()
+ let mutable i = 0
+ for c in result do
+ result.[i] <- mapping c
+ i <- i + 1
+
+ new String(result)
[]
let mapi (mapping: int -> char -> char) (str:string) =
@@ -53,13 +62,30 @@ namespace Microsoft.FSharp.Core
[]
let filter (predicate: char -> bool) (str:string) =
- if String.IsNullOrEmpty str then
+ let len = length str
+
+ if len = 0 then
String.Empty
- else
- let res = StringBuilder str.Length
+
+ elif len > LOH_CHAR_THRESHOLD then
+ // By using SB here, which is twice slower than the optimized path, we prevent LOH allocations
+ // and 'stop the world' collections if the filtering results in smaller strings.
+ // We also don't pre-allocate SB here, to allow for less mem pressure when filter result is small.
+ let res = StringBuilder()
str |> iter (fun c -> if predicate c then res.Append c |> ignore)
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 mutable i = 0
+ for c in str do
+ if predicate c then
+ target.[i] <- c
+ i <- i + 1
+
+ String(target, 0, i)
+
[]
let collect (mapping: char -> string) (str:string) =
if String.IsNullOrEmpty str then
@@ -81,13 +107,38 @@ namespace Microsoft.FSharp.Core
let replicate (count:int) (str:string) =
if count < 0 then invalidArgInputMustBeNonNegative "count" count
- if String.IsNullOrEmpty str then
+ let len = length str
+ if len = 0 || count = 0 then
String.Empty
+
+ elif len = 1 then
+ new String(str.[0], count)
+
+ elif count <= 4 then
+ match count with
+ | 1 -> str
+ | 2 -> String.Concat(str, str)
+ | 3 -> String.Concat(str, str, str)
+ | _ -> String.Concat(str, str, str, str)
+
else
- let res = StringBuilder(count * str.Length)
- for i = 0 to count - 1 do
- res.Append str |> ignore
- res.ToString()
+ // 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)
+ let source = str.ToCharArray()
+
+ // 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
+ Array.Copy(source, 0, target, 0, len)
+ let mutable i = len
+ while i * 2 < target.Length do
+ Array.Copy(target, 0, target, i, i)
+ i <- i * 2
+
+ // finally, copy the remain half, or less-then half
+ Array.Copy(target, 0, target, i, target.Length - i)
+ new String(target)
+
[]
let forall predicate (str:string) =
diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs
index a72ab64e236..6c1b1360585 100644
--- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs
+++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/StringModule.fs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace FSharp.Core.UnitTests.Collections
@@ -71,11 +71,35 @@ type StringModule() =
[]
member this.Map() =
- let e1 = String.map (fun c -> c) "foo"
- Assert.AreEqual("foo", e1)
+ let e1 = String.map id "xyz"
+ Assert.AreEqual("xyz", e1)
- let e2 = String.map (fun c -> c) null
- Assert.AreEqual("", e2)
+ let e2 = String.map (fun c -> c + char 1) "abcde"
+ Assert.AreEqual("bcdef", e2)
+
+ let e3 = String.map (fun c -> c) null
+ Assert.AreEqual("", e3)
+
+ let e4 = String.map (fun c -> c) String.Empty
+ Assert.AreEqual("", e4)
+
+ let e5 = String.map (fun _ -> 'B') "A"
+ Assert.AreEqual("B", e5)
+
+ let e6 = String.map (fun _ -> failwith "should not raise") null
+ Assert.AreEqual("", e6)
+
+ // this tests makes sure mapping function is not called too many times
+ let mutable x = 0
+ let e7 = String.map (fun _ -> if x > 2 then failwith "should not raise" else x <- x + 1; 'x') "abc"
+ Assert.AreEqual(x, 3)
+ Assert.AreEqual(e7, "xxx")
+
+ // side-effect and "order of operation" test
+ let mutable x = 0
+ let e8 = String.map (fun c -> x <- x + 1; c + char x) "abcde"
+ Assert.AreEqual(x, 5)
+ Assert.AreEqual(e8, "bdfhj")
[]
member this.MapI() =
@@ -87,18 +111,25 @@ type StringModule() =
[]
member this.Filter() =
- let e1 = String.filter (fun c -> true) "foo"
- Assert.AreEqual("foo", e1)
+ let e1 = String.filter (fun c -> true) "Taradiddle"
+ Assert.AreEqual("Taradiddle", e1)
let e2 = String.filter (fun c -> true) null
Assert.AreEqual("", e2)
- let e3 = String.filter (fun c -> c <> 'o') "foo bar"
- Assert.AreEqual("f bar", e3)
+ let e3 = String.filter Char.IsUpper "How Vexingly Quick Daft Zebras Jump!"
+ Assert.AreEqual("HVQDZJ", e3)
let e4 = String.filter (fun c -> c <> 'o') ""
Assert.AreEqual("", e4)
+ let e5 = String.filter (fun c -> c > 'B' ) "ABRACADABRA"
+ Assert.AreEqual("RCDR", e5)
+
+ // LOH test with 55k string, which is 110k bytes
+ let e5 = String.filter (fun c -> c > 'B' ) (String.replicate 5_000 "ABRACADABRA")
+ Assert.AreEqual(String.replicate 5_000 "RCDR", e5)
+
[]
member this.Collect() =
let e1 = String.collect (fun c -> "a"+string c) "foo"
@@ -125,15 +156,42 @@ type StringModule() =
[]
member this.Replicate() =
- let e1 = String.replicate 0 "foo"
+ let e1 = String.replicate 0 "Snickersnee"
Assert.AreEqual("", e1)
- let e2 = String.replicate 2 "foo"
- Assert.AreEqual("foofoo", e2)
+ let e2 = String.replicate 2 "Collywobbles, "
+ Assert.AreEqual("Collywobbles, Collywobbles, ", e2)
let e3 = String.replicate 2 null
Assert.AreEqual("", e3)
+ let e4 = String.replicate 300_000 ""
+ Assert.AreEqual("", e4)
+
+ let e5 = String.replicate 23 "天地玄黃,宇宙洪荒。"
+ Assert.AreEqual(230 , e5.Length)
+ Assert.AreEqual("天地玄黃,宇宙洪荒。天地玄黃,宇宙洪荒。", e5.Substring(0, 20))
+
+ // This tests the cut-off point for the O(log(n)) algorithm with a prime number
+ let e6 = String.replicate 84673 "!!!"
+ Assert.AreEqual(84673 * 3, e6.Length)
+
+ // This tests the cut-off point for the O(log(n)) algorithm with a 2^x number
+ let e7 = String.replicate 1024 "!!!"
+ Assert.AreEqual(1024 * 3, e7.Length)
+
+ let e8 = String.replicate 1 "What a wonderful world"
+ Assert.AreEqual("What a wonderful world", e8)
+
+ let e9 = String.replicate 3 "أضعت طريقي! أضعت طريقي" // means: I'm lost
+ Assert.AreEqual("أضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقي", e9)
+
+ let e10 = String.replicate 4 "㏖ ㏗ ℵ "
+ Assert.AreEqual("㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ", e10)
+
+ let e11 = String.replicate 5 "5"
+ Assert.AreEqual("55555", e11)
+
CheckThrowsArgumentException(fun () -> String.replicate -1 "foo" |> ignore)
[]