Skip to content

Commit de5dc3b

Browse files
authored
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
1 parent 35e2caa commit de5dc3b

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

src/fsharp/FSharp.Core/string.fs

+30-5
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,38 @@ namespace Microsoft.FSharp.Core
107107
let replicate (count:int) (str:string) =
108108
if count < 0 then invalidArgInputMustBeNonNegative "count" count
109109

110-
if String.IsNullOrEmpty str then
110+
let len = length str
111+
if len = 0 || count = 0 then
111112
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+
112124
else
113-
let res = StringBuilder(count * str.Length)
114-
for i = 0 to count - 1 do
115-
res.Append str |> ignore
116-
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+
117142

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

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

+31-4
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

@@ -156,15 +156,42 @@ type StringModule() =
156156

157157
[<Test>]
158158
member this.Replicate() =
159-
let e1 = String.replicate 0 "foo"
159+
let e1 = String.replicate 0 "Snickersnee"
160160
Assert.AreEqual("", e1)
161161

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

165165
let e3 = String.replicate 2 null
166166
Assert.AreEqual("", e3)
167167

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+
168195
CheckThrowsArgumentException(fun () -> String.replicate -1 "foo" |> ignore)
169196

170197
[<Test>]

0 commit comments

Comments
 (0)