Improving performance in .NET

Small changes can make your code 10x faster

Improving performance in .NET

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:

image.png

The first version using Substring takes less than half of the time of the second one using Convert.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:

image.png

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: