Skip to content

Commit

Permalink
reverse-string: add approaches
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikSchierboom committed Jan 18, 2024
1 parent 13b2527 commit e8a7a4d
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 81 deletions.
15 changes: 3 additions & 12 deletions exercises/practice/reverse-string/.approaches/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"approaches": [
{
"uuid": "44a0fe16-9c7d-4481-9eae-b223ecd3f88e",
"uuid": "85f2dd83-247d-4549-a6b3-ff6c4356288d",
"slug": "seq-module",
"title": "Seq module",
"blurb": "Use functions from the Seq module to concisely reverse a string.",
Expand All @@ -15,16 +15,7 @@
]
},
{
"uuid": "8a2ddcd5-ffe7-4448-842e-25b302708b8c",
"slug": "array-reverse",
"title": "Array.Reverse",
"blurb": "Use Array.Reverse to reverse a string.",
"authors": [
"erikschierboom"
]
},
{
"uuid": "f42fa12e-e9b8-4aaa-9fc2-188c67d05809",
"uuid": "c4c26760-3398-46ba-8d9c-445c7b17860a",
"slug": "span",
"title": "Span<T>",
"blurb": "Use Span<T> and stack allocation for hyper-optimized string reversal.",
Expand All @@ -33,7 +24,7 @@
]
},
{
"uuid": "f4f662d0-4917-4a89-a62a-12809a8c2b11",
"uuid": "fd88f91d-57e4-4e16-95b3-e8cf5046a82c",
"slug": "string-builder",
"title": "StringBuilder",
"blurb": "Reverse a string using the StringBuilder class.",
Expand Down
23 changes: 16 additions & 7 deletions exercises/practice/reverse-string/.approaches/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,28 @@ let reverse (input: string) =
This approach iterates over the string's characters backwards, building up the reverse string using a `StringBuilder`.
For more information, check the [`StringBuilder` approach][approach-string-builder].

## Which approach to use?
## Alternative approach: `Span<T>`

If readability is your primary concern (and it usually should be), the `Seq` module approach is hard to beat.
```fsharp
let reverse (input: string) =
let memory = NativePtr.stackalloc<byte>(input.Length) |> NativePtr.toVoidPtr
let span = Span<char>(memory, input.Length)
for i in 0..input.Length - 1 do
span[input.Length - 1 - i] <- input[i]
The `Array.Reverse()` approach is the best performing apporach.
For a more detailed breakdown, check the [performance article][article-performance].
span.ToString()
```

This approach uses the `Span<T>` type, which is a highly optimized type designed to have great performance.
For more information, check the [`Span<T>` approach][approach-span].

The `StringBuilder` approach has the worst performance of the listed approach, and is more error-prone to write as it has to deal with lower and upper bounds checking.
## Which approach to use?

If readability is your primary concern (and it usually should be), the `Seq` module approach is hard to beat.

[constructor-array-chars]: https://learn.microsoft.com/en-us/dotnet/api/system.string.-ctor
[article-performance]: https://exercism.org/tracks/fsharp/exercises/reverse-string/articles/performance
[approach-seq-module]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/seq-module
[approach-array-reverse]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/array-reverse
[approach-span]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/span
[approach-string-builder]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/string-builder
[seq-module]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,14 @@ let reverse input =
|> System.String
```

The `string` class implements the `IEnumerable<char>` interface, which allows us to call [LINQ][linq]'s [`Reverse()`][linq-reverse] extension method on it.
The `string` class implements the `seq` interface (which is an abbreviation of the CLI `IEnumerable` interface), which means we can use functions from the [`Seq` module][seq-module] on it.

To convert the `IEnumerable<char>` returned by `Reverse()` back to a `string`, we first use [`ToArray()`][linq-to-array] to convert it to a `char[]`.
First, we pipe the input `string` into [`Seq.reverse`][seq.rev], which returns an enumerable with the input in reverse order.

Finally, we return the reversed `string` by calling its constructor with the (reversed) `char[]`.
To convert the `seq<char>` returned by `Seq.reverse` back to a `string`, we first use [`Seq.toArray`][seq.toArray] to convert it to a `char[]`.

## Shortening
Finally, we convert the `char` array back to a `string` by piping it into the `System.String` constructor.

There are two things we can do to further shorten this method:

1. Remove the curly braces by converting to an [expression-bodied method][expression-bodied-method]
1. Use a [target-typed new][target-typed-new] expression to replace `new string` with just `new` (the compiler can figure out the type from the method's return type)

Using this, we end up with:

```fsharp
public static string Reverse(string input) => new(input.Reverse().ToArray());
```

## Performance

If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance].

[linq-reverse]: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.reverse
[linq-to-array]: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.toarray
[expression-bodied-method]: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members#methods
[linq]: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/
[target-typed-new]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new
[approach-performance]: https://exercism.org/tracks/csharp/exercises/reverse-string/articles/performance
[seq-module]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html
[seq.rev]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#rev
[seq.toArray]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#toArray
38 changes: 13 additions & 25 deletions exercises/practice/reverse-string/.approaches/span/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,35 @@ let reverse (input: string) =
span.ToString()
```

C# 7.2. introduced the [`Span<T>`][span-t] class, which was specifically designed to allow performant iteration/mutation of _array-like_ objects.
F# 4.5 introduced support for the [`Span<T>`][span-t] class, which was specifically designed to allow performant iteration/mutation of _array-like_ objects.
The `Span<T>` class helps improve performance by always being allocated on the _stack_, and not the _heap_.
As objects on the stack don't need to be garbage collected, this can help improve performance (check [this blog post][using-span-t] for more information).

How can we leverage `Span<T>` to reverse our `string`?
The `string` class has an [`AsSpan()`][string-as-span] method, but that returns a `ReadOnlySpan<char>`, which doesn't allow mutation (otherwise we'd be able to indirectly modify the `string`).

We can work around this by manually allocating a `char[]` and assigning to a `Span<char>`:

```fsharp
Span<char> chars = new char[input.Length];
for (var i = 0; i < input.Length; i++)
{
chars[input.Length - 1 - i] = input[i];
}
return new string(chars);
let array = Array.zeroCreate<char>(input.Length)
let span = Span<char>(array)
for i in 0..input.Length - 1 do
span[input.Length - 1 - i] <- input[i]
span.ToString()
```

After creating `Span<char>`, we use a regular `for`-loop to iterate over the string's characters and assign them to the right position in the span.
Finally, we can use the `string` constructor overload that takes a `Span<char>` to create the `string`.

However, this is basically the same approach as the `Array.Reverse()` approach, but with us also having to manually write a `for`-loop.
We _can_ do one better though, and that is to use [`stackalloc`][stackalloc].
We _can_ do one better though, and that is to use [`NativePtr.stackalloc`]nativeptr.stackalloc.
With `stackalloc`, we can assign a block of memory _on the stack_ (whereas the array would be stored on the heap).

```fsharp
Span<char> chars = stackalloc char[input.Length];
let memory = NativePtr.stackalloc<byte>(input.Length) |> NativePtr.toVoidPtr
let span = Span<char>(memory, input.Length)
```

With this version, the memory allocated for the `Span<char>` is all on the stack and no garbage collection is needed for that data.
Expand All @@ -55,22 +58,7 @@ So what is the limit for the amount of memory we can allocate?
Well, this depends on how memory has already been allocated on the stack.
That said, a small test program successfully stack-allocated memory for `750_000` characters, so you might be fine.

## Alternative

It is possible to use an alternative span-based implementation that is more readable, but has the downside of being about twice as slow:

```fsharp
Span<char> chars = stackalloc char[input.Length];
input.AsSpan().CopyTo(chars);
chars.Reverse();
return new string(chars);
```

## Performance

If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance].

[stackalloc]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc
[nativeptr.stackalloc]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-nativeinterop-nativeptrmodule.html#stackalloc
[using-span-t]: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
[span-t]: https://learn.microsoft.com/en-us/dotnet/api/system.span-1
[string-as-span]: https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.asspan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ A `StringBuilder` is often overkill when used to create short strings, but can b
```

The first step is to create a `StringBuilder`.
We then use a `for`-loop to walk through the string's characters in reverse order, appending them to the `StringBuilder` via its [`Append()`][string-builder-append] method.
We then use a `for`-loop to walk through the string's characters in reverse order via the [`Seq.rev` function][seq.rev], appending them to the `StringBuilder` via its [`Append()`][string-builder-append] method.

Finally, we return the reversed `string` by calling the `ToString()` method on the `StringBuilder` instance.

## Performance

If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance].

[string-builder]: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder
[string-builder-append]: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append
[approach-performance]: https://exercism.org/tracks/csharp/exercises/reverse-string/articles/performance
[seq.rev]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#rev
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
var chars = new StringBuilder();
for (var i = input.Length - 1; i >= 0; i--)
{
chars.Append(input[i]);
}
return chars.ToString();
let reverse (input: string) =
let chars = StringBuilder()
for char in Seq.rev input do
chars.Append(char) |> ignore
chars.ToString()

0 comments on commit e8a7a4d

Please sign in to comment.