Improving performance in .NET
Small changes can make your code 10x faster
Table of contents
The problem
Recently a new requirement was brought up in a project I was working on: format a CPF (Brazilian National ID) , here's how it should be:
Input: “12345678909”
Output: “123.456.789–09”
Well then, there are multiple forms to do this implementation, for the first version I perhaps opted for the easier path:
public string FormatarCpf(string cpf)
{
// CPF: 12345678909
return $"{cpf.Substring(0, 3)}.{cpf.Substring(3, 3)}.{cpf.Substring(6, 3)}-{cpf.Substring(9, 2)}";
// result: 123.456.789-09
}
Basically, I just used Substring
for split the CPF into four parts and then inserting the corresponding separators. This implementation although effective doesn't appear very efficient, so I decided to improve it.
Let’s google it
As a good developer I went to search if something already existed in Google and, of course, I ended up in the good old Stack Overflow. I found an implementation quite different and even simpler:
public string FormatarCpf(string cpf)
{
return Convert.ToUInt64(cpf).ToString(@"000\.000\.000\-00");
}
This version also works perfectly and at first look it seems "ok", but when we take a closer look we could notice something important: For use of the number format string (that allows defining a mask for a number in the ToString
) the string is converted to UInt64
— that led me to many doubts related to performance, so, to discover if this change would really improve my code a decide to use Benchmark.DotNet
to compare the implementations. The result was worrying:
The first version using
Substring
takes less than half of the time of the second one usingConvert.ToInt64
.
That is, is not worth to change to that version by any means, but also, a mean of 115 nanoseconds for a simple string format doesn't appear to be ideal.
How can we improve it?
The main problem of the solution using Substring
is that each "piece" of the CPF generates a new allocation in the memory.
In order to apply improvements we need to go back to the original problem thinking about memory allocation: how to return a CPF string in a new array of strings, adding only the separators? To achieve this goal we need to avoid allocation and any type of conversion.
Fortunately there are new structures in C# like Span<T>
and stackalloc
that were created precisely for situations like this.
Using this new approach I ended up with this solution:
public ReadOnlySpan<char> FormatarCpf(string cpf)
{
Span<char> formattedValue = stackalloc char[14];
Copy(cpf, startIndex: 0, length: 3, ref formattedValue, insertIndex: 0);
formattedValue[3] = '.';
Copy(cpf, startIndex: 3, length: 3, ref formattedValue, insertIndex: 4);
formattedValue[7] = '.';
Copy(cpf, startIndex: 6, length: 3, ref formattedValue, insertIndex: 8);
formattedValue[11] = '-';
Copy(cpf, startIndex: 9, length: 2, ref formattedValue, insertIndex: 12);
return formattedValue.ToArray();
static void Copy(string origin, int startIndex, int length, ref Span<char> destination, int insertIndex)
{
for (int i = startIndex, j = insertIndex; i < (startIndex + length); i++, j++)
destination[j] = origin[i];
}
}
Main changes:
To store the formatted value I'm using a char array
Span<char>
allocated in stack memory and defining their size:stackalloc char[14]
— since we already know the full size.The original string values are copied one by one into the respective positions of the formatted array. I used a “Local Function” only to avoid repetition and optimize the processing.
The return value was modified to
ReadOnlySpan
— nothing more than a read-only char array.
Final result:
25 nanoseconds! 10x faster than the version with conversion and 4x faster than the original version with substring.
Conclusion: it is worth spending time understanding better how the language works and optimizing your code to deliver quality software.
The complete code and tests with metrics are available on GitHub.
Referências: