Introduction
Reversing a string in C# might sound straightforward, but when efficiency matters, there are surprisingly many ways to do it. This complexity arises from the different definitions of efficiency, such as speed and memory usage, various ways characters are represented in different character sets, and different runtimes like Unity’s Mono.
In this post, I’ll explore several methods for reversing a string that are commonly found online. I’ll also provide some benchmark results for these methods in .NET 8. However, you shouldn’t rely solely on these benchmarks; it’s crucial to benchmark the algorithms yourself. The results can vary not only across different .NET versions but also on different hardware, especially since some of the more optimized methods depend on low-level hardware implementations. Additionally, the performance will vary depending on the size of the string you want to reverse, so make sure to benchmark these methods with strings that are similar in size to those you expect to work with.
Before diving into the examples, let’s define what we mean by efficiency and discuss some important nuances of the character set that C# uses.
Definition Of Efficiency
When I refer to efficiency in the context of string reversal in this post, I’m focusing on execution speed and heap memory allocation. General memory usage, such as temporary 16-bit local variables stored on the stack, is less of a concern since memory constraints are not as tight as they once were.
I’m also discussing the algorithm’s efficiency, not the programmer’s efficiency. Some of these algorithms can be challenging to understand if you’re not familiar with how they work, and more importantly, they can be difficult to modify if the requirements change—especially when dealing with characters outside the Basic Multilingual Plane. Generally, a programmer’s efficiency is more important than the program’s efficiency.
Unless your code is in a hot path, your string reversal is the bottleneck, or its execution time is significantly greater than other factors affecting performance in your code, the choice of algorithm should prioritize programmer efficiency. Specifically, consider how easy it is to modify, debug, and test your string reversal code.
Always benchmark your specific code on your target hardware, not only because different algorithms yield different results, but also because a more performant algorithm may have minimal or even no impact in your particular situation.
UTF16 Character Set
.NET uses the UTF-16 to store characters and strings. In UTF-16, characters require either one or two 16-bit words for representation, but a char in .NET is only 16 bits in length. This means some UTF-16 characters need two char values, known as surrogates. This has two implications for string handling:
- The length of a string may not correspond to the actual number of characters it contains. Some strings may have a greater length than the number of characters due to the presence of surrogate pairs.
- Since a single char is insufficient to represent a non-BMP (Basic Multilingual Plane) character, special care must be taken to treat two consecutive char values as a single character, rather than reversing each 16-bit word individually.
While this might sound complex, most programs can ignore these details because the majority of commonly used characters fall within the BMP. The BMP includes characters from many languages and over 30,000 Chinese characters. However, it doesn’t include characters from some ancient languages, musical notation symbols, or many emojis. You should assess your program’s requirements, and if non-BMP characters are needed, you can use the following static methods in the char type to detect non-BMP characters:
You can also use the following two char static methods to convert from a 32-bit value to two chars or the reverse:
- string ConvertFromUtf32 (int utf32)
- int ConvertToUtf32 (char highSurrogate, char lowSurrogate) or int ConvertToUtf32 (string s, int index)
For a more low-level approach, surrogates are easy to identify since each word falls within the range 0xD800
to 0xDFFF
. However, using the IsSurrogate
method on a span of characters is the most efficient way to handle them.
In the following string reversal algorithms, I do not account for surrogates. These algorithms assume that the input strings contain only characters within the BMP. This is not just because non-BMP characters are rare and a special case, but also because testing would be challenging—it would depend on the number of non-BMP characters relative to the string’s length.
Moreover, implementing logic to handle surrogate characters would significantly increase the complexity of some algorithms. While handling surrogates is relatively straightforward for algorithms that process individual char values, those that use the Reverse method of the internal SpanHelpers
class in .NET internally, would require substantial modifications. This method relies on marshalling memory and utilizing hardware registers, so accommodating surrogates would transform a simple statement into a series of complex logic steps, severely impacting performance.
With that in mind, let’s start with the simplest approach and build from there.
Simple Reversals
Let’s start with the least performant ways, that are also the more convenient to use. These methods are slow, but are the easiest to understand and use classes that most commonly come to mind when we have to deal with strings.
Adding To A String
One of the simplest and less performant ways is the following:
public static string StringSimpleReversal(string text)
{
string result = String.Empty;
for (int i = text.Length - 1; i >= 0; i--)
{
result += text[i];
}
return result;
}
Anyone with basic knowledge of C#, knows about the string immutability and how they affect the garbage collector. There are many different ways that someone can use string addition, this is just one example, but in the end it will always be the least performant way.I’m including this method here solely as a reference point, to provide a baseline for comparing the performance of other methods.
This approach involves traversing the original string backward and appending each character to a result
string that starts off as empty.
Using The Reverse Enumerable Extension Method
The next method, reverses a string by using the Enumerable.Reverse method. This method is simpler to understand than the previous, a little more performant too:
public static string ReverseEnumerable(string text) => new(text.Reverse().ToArray());
As simple as it gets, only one statement and easy to understand.
Using StringBuilder
The next method that comes to mind, when dealing with strings is to use the StringBuilder class:
public static string StringBuilderReverseMethod(string text)
{
var result = new StringBuilder(text.Length);
for (int i = text.Length - 1; i >= 0; i--)
{
result.Append(text[i]);
}
return result.ToString();
}
Works the same way as our first method, only now, we don’t have to deal with the garbage collector as much, because of the way StringBuilder
handles strings. More performant than the previous two methods, but still pretty slow compared to the “fast” reversal methods below.
Using A Temporary Value For Swapping Characters
After covering the more straightforward methods for string reversal, let’s explore some more efficient techniques. In this method, we use a temporary char variable to swap the positions of two characters. The loop iterates through the string, swapping the first character with the last, the second with the second-to-last, and so on, until it reaches the middle of the string. At this point, all the characters in the string have been reversed.
public static string ReverseStringWithTemp(string value)
{
Span<char> charSpan = value.ToCharArray();
int left = 0;
int right = charSpan.Length - 1;
while (left < right)
{
char temp = charSpan[left];
charSpan[left] = charSpan[right];
charSpan[right] = temp;
left++;
right--;
}
return new string(charSpan);
}
Interestingly, this method is the one proposed from chatGTP as the most performant way to reverse a string in C#. HINT: It is not. It is pretty performant, but not the most performant and its performance deteriorates in comparison to other methods below, as our strings get bigger.
Same With The Use Of Deconstructor
A variation of the above method, is to use a deconstructor to handle the reversal of two chars:
public static string ReverseStringDeconstruct(string value)
{
Span<char> charSpan = value.ToCharArray();
int left = 0;
int right = charSpan.Length - 1;
while (left < right)
{
(charSpan[left], charSpan[right]) = (charSpan[right], charSpan[left]);
left++;
right--;
}
return new string(charSpan);
}
This is easier to read than the previous one, but a little slower. The difference in performance compared to the previous is negligible (a few nanoseconds), this happens because if we check the IL code, we have two temporary variables created from the deconstruction. Use whatever seems easier to read to you, between those two methods.
Using XOR
Another way to reverse a string is by using a loop to swap the string’s characters, but this time with a XOR-based approach.
A XOR-based reversal can be particularly useful when dealing with very restrictive memory requirements, as it doesn’t require any temporary variables to swap two characters. Although this method is slower than the previous one, it’s valuable to understand because it can be applied to swap any two values, regardless of their type.
The XOR approach is based on the principle that the XOR operation is its own inverse. For example, if we have two values, A and B, and perform a XOR operation, we get a result: A XOR B = Result. If we then XOR the result with A, we retrieve B, and if we XOR it with B, we retrieve A. This property can be applied to string reversal as follows:
public static string ReverseXor(string s)
{
char[] charArray = s.ToCharArray();
int len = s.Length - 1;
for (int i = 0; i < len; i++, len--)
{
charArray[i] ^= charArray[len];
charArray[len] ^= charArray[i];
charArray[i] ^= charArray[len];
}
return new string(charArray);
}
Here’s how the XOR-based string reversal works: We take the character at the start of the string as A and the character at the end as B. First, we XOR them and store the result in A’s variable. Next, we XOR B with this result (now stored in A’s variable) and store this operation’s outcome in B’s variable. This gives us the original value of A in B’s variable, while A still holds the XOR result. Finally, we XOR the value now in A’s variable with the value now in B’s variable (which holds the original A). The result of this operation, which is the original value of B, is stored in A’s variable.
Using Array And Span Reverse
The following two methods, are based on the Reverse method of the internal SpanHelpers
class in .NET, as this is the method that is eventually called to perform the reversal in every case. This is a highly optimized method, that uses aggressive inlining and checks what registers are supported by the hardware and uses them, depending on their availability. Their performance is about equal, they are the most performant methods from all the above and the bigger the strings we have the greater the differences in performance gains compared to the previous methods.
public static string ReverseArray(string text)
{
char[] array = text.ToCharArray();
Array.Reverse(array);
return new string(array);
}
public static string ReverseSpan(string text)
{
Span<char> charSpan = text.ToCharArray();
charSpan.Reverse();
return charSpan.ToString();
}
Don’t get confused about the choice between using the Array.Reverse
and the charSpan.Reverse
as both of those under the hood use the same method. The Reverse method of the internal SpanHelpers
class, will use the Vector128 Vector256 or Vector512 structs depending on your hardware, and make the reversal by using the shuffle
method of those classes, along with some low level optimizations for passing the data.
Using The String.Create Method
Finally, I left the most performant method for last:
public static string StringReverseWithCreate(string text)
{
return string.Create(text.Length, text, (chars, text) =>
{
text.AsSpan().CopyTo(chars);
chars.Reverse();
});
}
This method is using the String.Create method. This method, creates a string faster and with no additional allocation, as it uses a Span and unsafe operations internally to get the raw string data and then uses the same reverse method from the internal SpanHelpers
class as the two previous methods. The effect is that, we have less heap memory allocations and our reversal executes faster.
The last three methods presented here, that use the reverse method from the internal SpanHelpers
class, are the fastest, but they will be really difficult to modify for use with non-BMP characters, as they reverse everything in chunks. We would need a way to create strings that don’t have surrogates, pass those strings to these methods and deal with the surrogates manually. This would be a problem that would increase both the complexity and the execution time of our program, so for strings that are expected to have surrogate characters, modifying one of the methods that deal directly with the char
type is preferable.
Benchmark Results
Finally some benchmarks. As I mentioned, benchmarks will differ depending on the hardware used, the length of the provided strings, the runtime and the .NET version. These are just for informational purposes and it is recommended that you run your own benchmarks on your own environment to get a more accurate view of each method’s performance.
Click to view the results for a random string with a length of 1
Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
---|---|---|---|---|---|---|
ReverseStringSimple | 13.19 ns | 0.298 ns | 0.264 ns | 13.11 ns | 0.0076 | 24 B |
ReverseStringEnumerable | 97.68 ns | 2.006 ns | 2.536 ns | 97.05 ns | 0.0535 | 168 B |
ReverseStringBuilder | 31.92 ns | 0.673 ns | 1.196 ns | 31.93 ns | 0.0331 | 104 B |
ReverseStringWithTemp | 23.84 ns | 0.510 ns | 0.664 ns | 24.00 ns | 0.0179 | 56 B |
ReverseStringDeconstruct | 23.39 ns | 0.446 ns | 0.395 ns | 23.54 ns | 0.0179 | 56 B |
ReverseStringXor | 23.37 ns | 0.450 ns | 0.399 ns | 23.27 ns | 0.0179 | 56 B |
ReverseStringArray | 24.85 ns | 0.518 ns | 0.692 ns | 24.87 ns | 0.0179 | 56 B |
ReverseStringSpan | 27.24 ns | 0.549 ns | 0.513 ns | 27.11 ns | 0.0179 | 56 B |
ReverseStringCreate | 11.66 ns | 0.269 ns | 0.450 ns | 11.46 ns | 0.0076 | 24 B |
Click to view the results for a random string with a length of 5
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 62.23 ns | 1.291 ns | 2.519 ns | 0.0484 | 152 B |
ReverseStringEnumerable | 155.12 ns | 3.067 ns | 2.869 ns | 0.0713 | 224 B |
ReverseStringBuilder | 43.43 ns | 0.895 ns | 1.132 ns | 0.0382 | 120 B |
ReverseStringWithTemp | 29.11 ns | 0.620 ns | 1.195 ns | 0.0229 | 72 B |
ReverseStringDeconstruct | 28.74 ns | 0.607 ns | 0.889 ns | 0.0229 | 72 B |
ReverseStringXor | 31.52 ns | 0.662 ns | 1.292 ns | 0.0229 | 72 B |
ReverseStringArray | 29.42 ns | 0.518 ns | 0.459 ns | 0.0229 | 72 B |
ReverseStringSpan | 33.33 ns | 0.701 ns | 1.112 ns | 0.0229 | 72 B |
ReverseStringCreate | 15.64 ns | 0.352 ns | 0.643 ns | 0.0102 | 32 B |
Click to view the results for a random string with a length of 10
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 122.36 ns | 1.901 ns | 1.686 ns | 0.1147 | 360 B |
ReverseStringEnumerable | 197.44 ns | 3.922 ns | 4.516 ns | 0.0918 | 288 B |
ReverseStringBuilder | 58.41 ns | 1.213 ns | 3.130 ns | 0.0459 | 144 B |
ReverseStringWithTemp | 35.53 ns | 0.748 ns | 1.748 ns | 0.0306 | 96 B |
ReverseStringDeconstruct | 36.74 ns | 0.767 ns | 1.441 ns | 0.0306 | 96 B |
ReverseStringXor | 38.86 ns | 0.634 ns | 0.562 ns | 0.0306 | 96 B |
ReverseStringArray | 37.55 ns | 0.771 ns | 1.245 ns | 0.0306 | 96 B |
ReverseStringSpan | 40.00 ns | 0.830 ns | 1.387 ns | 0.0306 | 96 B |
ReverseStringCreate | 19.45 ns | 0.418 ns | 0.371 ns | 0.0153 | 48 B |
Click to view the results for a random string with a length of 20
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 264.08 ns | 3.724 ns | 3.484 ns | 0.2933 | 920 B |
ReverseStringEnumerable | 292.97 ns | 5.717 ns | 5.871 ns | 0.1373 | 432 B |
ReverseStringBuilder | 78.20 ns | 1.595 ns | 1.638 ns | 0.0560 | 176 B |
ReverseStringWithTemp | 42.76 ns | 0.876 ns | 0.860 ns | 0.0408 | 128 B |
ReverseStringDeconstruct | 41.82 ns | 0.379 ns | 0.336 ns | 0.0408 | 128 B |
ReverseStringXor | 53.30 ns | 0.884 ns | 0.826 ns | 0.0408 | 128 B |
ReverseStringArray | 38.69 ns | 0.808 ns | 1.951 ns | 0.0408 | 128 B |
ReverseStringSpan | 39.69 ns | 0.776 ns | 0.797 ns | 0.0408 | 128 B |
ReverseStringCreate | 18.99 ns | 0.225 ns | 0.200 ns | 0.0204 | 64 B |
Click to view the results for a random string with a length of 30
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 435.15 ns | 7.917 ns | 7.406 ns | 0.5350 | 1680 B |
ReverseStringEnumerable | 302.83 ns | 1.087 ns | 0.908 ns | 0.1526 | 480 B |
ReverseStringBuilder | 101.02 ns | 0.786 ns | 0.735 ns | 0.0714 | 224 B |
ReverseStringWithTemp | 50.98 ns | 0.289 ns | 0.257 ns | 0.0561 | 176 B |
ReverseStringDeconstruct | 52.13 ns | 0.508 ns | 0.475 ns | 0.0561 | 176 B |
ReverseStringXor | 67.75 ns | 1.127 ns | 1.054 ns | 0.0560 | 176 B |
ReverseStringArray | 39.49 ns | 0.390 ns | 0.365 ns | 0.0561 | 176 B |
ReverseStringSpan | 43.94 ns | 0.164 ns | 0.137 ns | 0.0561 | 176 B |
ReverseStringCreate | 21.53 ns | 0.244 ns | 0.228 ns | 0.0280 | 88 B |
Click to view the results for a random string with a length of 40
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 650.72 ns | 12.900 ns | 15.842 ns | 0.8411 | 2640 B |
ReverseStringEnumerable | 388.90 ns | 2.778 ns | 2.598 ns | 0.1912 | 600 B |
ReverseStringBuilder | 131.57 ns | 0.479 ns | 0.400 ns | 0.0815 | 256 B |
ReverseStringWithTemp | 62.61 ns | 0.453 ns | 0.424 ns | 0.0663 | 208 B |
ReverseStringDeconstruct | 63.09 ns | 0.361 ns | 0.282 ns | 0.0663 | 208 B |
ReverseStringXor | 85.10 ns | 1.068 ns | 0.999 ns | 0.0663 | 208 B |
ReverseStringArray | 45.48 ns | 0.244 ns | 0.191 ns | 0.0663 | 208 B |
ReverseStringSpan | 48.42 ns | 0.071 ns | 0.055 ns | 0.0663 | 208 B |
ReverseStringCreate | 24.56 ns | 0.108 ns | 0.090 ns | 0.0331 | 104 B |
Click to view the results for a random string with a length of 50
Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
---|---|---|---|---|---|---|
ReverseStringSimple | 1,093.42 ns | 25.522 ns | 75.251 ns | 1,112.69 ns | 1.2112 | 3800 B |
ReverseStringEnumerable | 483.19 ns | 9.684 ns | 15.078 ns | 485.91 ns | 0.2060 | 648 B |
ReverseStringBuilder | 170.81 ns | 3.381 ns | 6.182 ns | 173.18 ns | 0.0968 | 304 B |
ReverseStringWithTemp | 75.72 ns | 1.549 ns | 3.163 ns | 74.24 ns | 0.0815 | 256 B |
ReverseStringDeconstruct | 73.11 ns | 0.455 ns | 0.403 ns | 73.14 ns | 0.0815 | 256 B |
ReverseStringXor | 100.78 ns | 0.700 ns | 0.654 ns | 100.57 ns | 0.0815 | 256 B |
ReverseStringArray | 53.12 ns | 0.478 ns | 0.399 ns | 52.98 ns | 0.0816 | 256 B |
ReverseStringSpan | 57.38 ns | 0.968 ns | 0.858 ns | 57.22 ns | 0.0816 | 256 B |
ReverseStringCreate | 30.55 ns | 0.432 ns | 0.383 ns | 30.48 ns | 0.0408 | 128 B |
Click to view the results for a random string with a length of 100
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 2,340.57 ns | 22.962 ns | 19.174 ns | 4.0169 | 12600 B |
ReverseStringEnumerable | 657.08 ns | 13.165 ns | 15.673 ns | 0.3157 | 992 B |
ReverseStringBuilder | 266.68 ns | 1.979 ns | 1.653 ns | 0.1578 | 496 B |
ReverseStringWithTemp | 130.31 ns | 1.171 ns | 1.038 ns | 0.1428 | 448 B |
ReverseStringDeconstruct | 130.66 ns | 1.021 ns | 0.905 ns | 0.1428 | 448 B |
ReverseStringXor | 184.13 ns | 1.872 ns | 1.659 ns | 0.1428 | 448 B |
ReverseStringArray | 77.03 ns | 0.449 ns | 0.375 ns | 0.1428 | 448 B |
ReverseStringSpan | 87.03 ns | 1.071 ns | 1.001 ns | 0.1428 | 448 B |
ReverseStringCreate | 44.35 ns | 0.586 ns | 0.520 ns | 0.0714 | 224 B |
Click to view the results for a random string with a length of 200
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 7,080.09 ns | 40.533 ns | 31.645 ns | 14.4043 | 45200 B |
ReverseStringEnumerable | 1,115.08 ns | 21.869 ns | 31.364 ns | 0.5322 | 1672 B |
ReverseStringBuilder | 497.35 ns | 2.983 ns | 2.644 ns | 0.2851 | 896 B |
ReverseStringWithTemp | 237.31 ns | 1.759 ns | 1.469 ns | 0.2699 | 848 B |
ReverseStringDeconstruct | 237.65 ns | 3.459 ns | 2.889 ns | 0.2699 | 848 B |
ReverseStringXor | 340.99 ns | 5.452 ns | 4.553 ns | 0.2699 | 848 B |
ReverseStringArray | 127.70 ns | 0.508 ns | 0.425 ns | 0.2701 | 848 B |
ReverseStringSpan | 132.87 ns | 1.486 ns | 1.241 ns | 0.2701 | 848 B |
ReverseStringCreate | 74.15 ns | 1.237 ns | 1.096 ns | 0.1351 | 424 B |
Click to view the results for a random string with a length of 500
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 38,178.8 ns | 507.39 ns | 449.78 ns | 83.8013 | 256.84 KB |
ReverseStringEnumerable | 2,286.6 ns | 16.11 ns | 14.28 ns | 1.1139 | 3.41 KB |
ReverseStringBuilder | 1,220.5 ns | 12.04 ns | 10.06 ns | 0.6676 | 2.05 KB |
ReverseStringWithTemp | 566.1 ns | 8.04 ns | 7.52 ns | 0.6523 | 2 KB |
ReverseStringDeconstruct | 570.6 ns | 3.92 ns | 3.27 ns | 0.6523 | 2 KB |
ReverseStringXor | 832.5 ns | 5.91 ns | 4.93 ns | 0.6523 | 2 KB |
ReverseStringArray | 310.1 ns | 6.08 ns | 5.08 ns | 0.6523 | 2 KB |
ReverseStringSpan | 319.3 ns | 6.14 ns | 6.03 ns | 0.6523 | 2 KB |
ReverseStringCreate | 173.7 ns | 1.73 ns | 1.53 ns | 0.3262 | 1 KB |
Click to view the results for a random string with a length of 1000
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
ReverseStringSimple | 142,143.8 ns | 689.67 ns | 611.37 ns | 326.9043 | 1001.95 KB |
ReverseStringEnumerable | 4,252.4 ns | 59.55 ns | 79.49 ns | 2.0828 | 6.39 KB |
ReverseStringBuilder | 2,390.0 ns | 22.35 ns | 17.45 ns | 1.3046 | 4 KB |
ReverseStringWithTemp | 1,120.2 ns | 14.06 ns | 13.15 ns | 1.2894 | 3.95 KB |
ReverseStringDeconstruct | 1,113.1 ns | 4.70 ns | 3.67 ns | 1.2894 | 3.95 KB |
ReverseStringXor | 1,630.4 ns | 8.60 ns | 7.18 ns | 1.2894 | 3.95 KB |
ReverseStringArray | 592.5 ns | 6.55 ns | 5.81 ns | 1.2903 | 3.95 KB |
ReverseStringSpan | 599.2 ns | 4.23 ns | 3.53 ns | 1.2903 | 3.95 KB |
ReverseStringCreate | 345.5 ns | 3.70 ns | 3.28 ns | 0.6452 | 1.98 KB |
Conclusion
These are the different ways that I have found to perform string reversal in C#. As you can see, it is not that simple, as they don’t only depend on hardware and input length, as all benchmarks do, but we also have to take into account the characters those strings contain and more specifically if they belong in the Basic Multilingual Plane. Generally, if surrogate characters are of no concern, then the methods from the BCL that use the Reversal method of the internal SpanHelpers class are preferable, as it contains low level optimizations and will get upgrades over time if needed.
If you know more performant ways to reverse a string, or you get different benchmark results in your hardware I would love if you could leave a comment below. Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.