Skip to content

Commit b74c9de

Browse files
dotnet-botdotnet-maestro[bot]abelbraaksma
authored
Merge master to release/dev16.7 (#9578)
* Update dependencies from https://github.com/dotnet/arcade build 20200626.2 (#9577) Microsoft.DotNet.Arcade.Sdk From Version 1.0.0-beta.20302.3 -> To Version 1.0.0-beta.20326.2 Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> * 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 * String map performance improvement (#9470) * Simplify and improve perf of String.length * Improve performance of String.map * Revert "Simplify and improve perf of String.length" * Resolves #9470 (comment) * Lingering space * Change `String` to use `new` to clarify use of ctor * Add some better tests for String.map, add side-effect test * Add tests to ensure the mapping function is called a deterministically amount of times * Fix typo * Remove "foo" from String.map tests * Perf: String.replicate from O(n) to O(log(n)), up to 12x speed improvement (#9512) * Turn String.replicate from O(n) into O(log(n)) * Cleanup String.replicate tests by removing usages of "foo" * String.replicate: add tests for missing cases, and for the new O(log(n)) cut-off points * Improve String.replicate algorithm further * Add tests for String.replicate covering all lines/branches of algo * Fix accidental comment Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Abel Braaksma <abel.online@xs4all.nl>
1 parent 42f26f2 commit b74c9de

File tree

6 files changed

+137
-28
lines changed

6 files changed

+137
-28
lines changed

eng/Version.Details.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<ProductDependencies>
44
</ProductDependencies>
55
<ToolsetDependencies>
6-
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="1.0.0-beta.20302.3">
6+
<Dependency Name="Microsoft.DotNet.Arcade.Sdk" Version="1.0.0-beta.20326.2">
77
<Uri>https://github.com/dotnet/arcade</Uri>
8-
<Sha>9b71be0663493cd0e111b55536a2e1eeb272f54c</Sha>
8+
<Sha>ed69753a3ffbdaa08365252c710d57a64d17f859</Sha>
99
</Dependency>
1010
</ToolsetDependencies>
1111
</Dependencies>

eng/common/sdl/packages.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
3-
<package id="Microsoft.Guardian.Cli" version="0.7.2"/>
3+
<package id="Microsoft.Guardian.Cli.win10-x64" version="0.20.1"/>
44
</packages>

eng/common/templates/job/execute-sdl.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
continueOnError: ${{ parameters.sdlContinueOnError }}
6666
- ${{ if eq(parameters.overrideParameters, '') }}:
6767
- powershell: eng/common/sdl/execute-all-sdl-tools.ps1
68-
-GuardianPackageName Microsoft.Guardian.Cli.0.7.2
68+
-GuardianPackageName Microsoft.Guardian.Cli.win10-x64.0.20.1
6969
-NugetPackageDirectory $(Build.SourcesDirectory)\.packages
7070
-AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw)
7171
${{ parameters.additionalParameters }}

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010
},
1111
"msbuild-sdks": {
12-
"Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20302.3",
12+
"Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20326.2",
1313
"Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19069.2"
1414
}
1515
}

src/fsharp/FSharp.Core/string.fs

Lines changed: 62 additions & 11 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

@@ -37,9 +42,13 @@ namespace Microsoft.FSharp.Core
3742
if String.IsNullOrEmpty str then
3843
String.Empty
3944
else
40-
let res = StringBuilder str.Length
41-
str |> iter (fun c -> res.Append(mapping c) |> ignore)
42-
res.ToString()
45+
let result = str.ToCharArray()
46+
let mutable i = 0
47+
for c in result do
48+
result.[i] <- mapping c
49+
i <- i + 1
50+
51+
new String(result)
4352

4453
[<CompiledName("MapIndexed")>]
4554
let mapi (mapping: int -> char -> char) (str:string) =
@@ -53,13 +62,30 @@ namespace Microsoft.FSharp.Core
5362

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

78+
else
79+
// Must do it this way, since array.fs is not yet in scope, but this is safe
80+
let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len
81+
let mutable i = 0
82+
for c in str do
83+
if predicate c then
84+
target.[i] <- c
85+
i <- i + 1
86+
87+
String(target, 0, i)
88+
6389
[<CompiledName("Collect")>]
6490
let collect (mapping: char -> string) (str:string) =
6591
if String.IsNullOrEmpty str then
@@ -81,13 +107,38 @@ namespace Microsoft.FSharp.Core
81107
let replicate (count:int) (str:string) =
82108
if count < 0 then invalidArgInputMustBeNonNegative "count" count
83109

84-
if String.IsNullOrEmpty str then
110+
let len = length str
111+
if len = 0 || count = 0 then
85112
String.Empty
113+
114+
elif len = 1 then
115+
new String(str.[0], count)
116+
117+
elif count <= 4 then
118+
match count with
119+
| 1 -> str
120+
| 2 -> String.Concat(str, str)
121+
| 3 -> String.Concat(str, str, str)
122+
| _ -> String.Concat(str, str, str, str)
123+
86124
else
87-
let res = StringBuilder(count * str.Length)
88-
for i = 0 to count - 1 do
89-
res.Append str |> ignore
90-
res.ToString()
125+
// Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive.
126+
let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len * count)
127+
let source = str.ToCharArray()
128+
129+
// O(log(n)) performance loop:
130+
// Copy first string, then keep copying what we already copied
131+
// (i.e., doubling it) until we reach or pass the halfway point
132+
Array.Copy(source, 0, target, 0, len)
133+
let mutable i = len
134+
while i * 2 < target.Length do
135+
Array.Copy(target, 0, target, i, i)
136+
i <- i * 2
137+
138+
// finally, copy the remain half, or less-then half
139+
Array.Copy(target, 0, target, i, target.Length - i)
140+
new String(target)
141+
91142

92143
[<CompiledName("ForAll")>]
93144
let forall predicate (str:string) =

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

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
22

33
namespace FSharp.Core.UnitTests.Collections
44

@@ -71,11 +71,35 @@ type StringModule() =
7171

7272
[<Test>]
7373
member this.Map() =
74-
let e1 = String.map (fun c -> c) "foo"
75-
Assert.AreEqual("foo", e1)
74+
let e1 = String.map id "xyz"
75+
Assert.AreEqual("xyz", e1)
7676

77-
let e2 = String.map (fun c -> c) null
78-
Assert.AreEqual("", e2)
77+
let e2 = String.map (fun c -> c + char 1) "abcde"
78+
Assert.AreEqual("bcdef", e2)
79+
80+
let e3 = String.map (fun c -> c) null
81+
Assert.AreEqual("", e3)
82+
83+
let e4 = String.map (fun c -> c) String.Empty
84+
Assert.AreEqual("", e4)
85+
86+
let e5 = String.map (fun _ -> 'B') "A"
87+
Assert.AreEqual("B", e5)
88+
89+
let e6 = String.map (fun _ -> failwith "should not raise") null
90+
Assert.AreEqual("", e6)
91+
92+
// this tests makes sure mapping function is not called too many times
93+
let mutable x = 0
94+
let e7 = String.map (fun _ -> if x > 2 then failwith "should not raise" else x <- x + 1; 'x') "abc"
95+
Assert.AreEqual(x, 3)
96+
Assert.AreEqual(e7, "xxx")
97+
98+
// side-effect and "order of operation" test
99+
let mutable x = 0
100+
let e8 = String.map (fun c -> x <- x + 1; c + char x) "abcde"
101+
Assert.AreEqual(x, 5)
102+
Assert.AreEqual(e8, "bdfhj")
79103

80104
[<Test>]
81105
member this.MapI() =
@@ -87,18 +111,25 @@ type StringModule() =
87111

88112
[<Test>]
89113
member this.Filter() =
90-
let e1 = String.filter (fun c -> true) "foo"
91-
Assert.AreEqual("foo", e1)
114+
let e1 = String.filter (fun c -> true) "Taradiddle"
115+
Assert.AreEqual("Taradiddle", e1)
92116

93117
let e2 = String.filter (fun c -> true) null
94118
Assert.AreEqual("", e2)
95119

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

99123
let e4 = String.filter (fun c -> c <> 'o') ""
100124
Assert.AreEqual("", e4)
101125

126+
let e5 = String.filter (fun c -> c > 'B' ) "ABRACADABRA"
127+
Assert.AreEqual("RCDR", e5)
128+
129+
// LOH test with 55k string, which is 110k bytes
130+
let e5 = String.filter (fun c -> c > 'B' ) (String.replicate 5_000 "ABRACADABRA")
131+
Assert.AreEqual(String.replicate 5_000 "RCDR", e5)
132+
102133
[<Test>]
103134
member this.Collect() =
104135
let e1 = String.collect (fun c -> "a"+string c) "foo"
@@ -125,15 +156,42 @@ type StringModule() =
125156

126157
[<Test>]
127158
member this.Replicate() =
128-
let e1 = String.replicate 0 "foo"
159+
let e1 = String.replicate 0 "Snickersnee"
129160
Assert.AreEqual("", e1)
130161

131-
let e2 = String.replicate 2 "foo"
132-
Assert.AreEqual("foofoo", e2)
162+
let e2 = String.replicate 2 "Collywobbles, "
163+
Assert.AreEqual("Collywobbles, Collywobbles, ", e2)
133164

134165
let e3 = String.replicate 2 null
135166
Assert.AreEqual("", e3)
136167

168+
let e4 = String.replicate 300_000 ""
169+
Assert.AreEqual("", e4)
170+
171+
let e5 = String.replicate 23 "天地玄黃,宇宙洪荒。"
172+
Assert.AreEqual(230 , e5.Length)
173+
Assert.AreEqual("天地玄黃,宇宙洪荒。天地玄黃,宇宙洪荒。", e5.Substring(0, 20))
174+
175+
// This tests the cut-off point for the O(log(n)) algorithm with a prime number
176+
let e6 = String.replicate 84673 "!!!"
177+
Assert.AreEqual(84673 * 3, e6.Length)
178+
179+
// This tests the cut-off point for the O(log(n)) algorithm with a 2^x number
180+
let e7 = String.replicate 1024 "!!!"
181+
Assert.AreEqual(1024 * 3, e7.Length)
182+
183+
let e8 = String.replicate 1 "What a wonderful world"
184+
Assert.AreEqual("What a wonderful world", e8)
185+
186+
let e9 = String.replicate 3 "أضعت طريقي! أضعت طريقي" // means: I'm lost
187+
Assert.AreEqual("أضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقيأضعت طريقي! أضعت طريقي", e9)
188+
189+
let e10 = String.replicate 4 "㏖ ㏗ ℵ "
190+
Assert.AreEqual("㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ㏖ ㏗ ℵ ", e10)
191+
192+
let e11 = String.replicate 5 "5"
193+
Assert.AreEqual("55555", e11)
194+
137195
CheckThrowsArgumentException(fun () -> String.replicate -1 "foo" |> ignore)
138196

139197
[<Test>]

0 commit comments

Comments
 (0)