Skip to content

Commit

Permalink
Perf: String.replicate from O(n) to O(log(n)), up to 12x speed improv…
Browse files Browse the repository at this point in the history
…ement (#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
  • Loading branch information
abelbraaksma authored Jun 27, 2020
1 parent 35e2caa commit de5dc3b
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 9 deletions.
35 changes: 30 additions & 5 deletions src/fsharp/FSharp.Core/string.fs
Original file line number Diff line number Diff line change
Expand Up @@ -107,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)


[<CompiledName("ForAll")>]
let forall predicate (str:string) =
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -156,15 +156,42 @@ type StringModule() =

[<Test>]
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)

[<Test>]
Expand Down

0 comments on commit de5dc3b

Please sign in to comment.