Há algum tempo aponto o “abuso” do GC é uma das principais causa para problemas de performance em produção.
Nesse post, discuto formas eficientes (e não eficientes) para inverter strings. (SPOILER: Vamos obter um ganho de 18x)
Este post é baseado em uma excelente apresentação do Ayende, ainda não disponível em vídeo.
Usando LINQ
A forma mais simples de se inverter uma string, acredito, em C# é usando LINQ.
public static string NaiveRerverseString(string input) =>
new string(input.Reverse().ToArray());
O problema dessa abordagem é que ela faz três alocações.
- o new string para gerar o valor de retorno
- a função Reverse que aloca um enumerator de chars
- a função ToArray que cria um array da enumeração de chars gerada pela Reverse.
Estamos gerando pressão para o GC e isso não é uma coisa boa.
Dispensando LINQ
Minha primeira abordagem para melhorar a performance da função seria dispensar LINQ (que não está agregando tanto valor aqui). Vejamos:
public static string CharReverseString(string input)
{
var reversedCharArray = new char[input.Length];
for (
int i = input.Length - 1, destIndex = 0;
i >= 0;
i--, destIndex++
)
{
reversedCharArray[destIndex] = input[i];
}
return new string(reversedCharArray);
}
Esta abordagem é sim um pouco mais complexa (e complexidade é custo!). Mas, remove o LINQ da solução e, consequentemente, algumas alocações.
Usando Stack no lugar da Heap
Sempre que possível é interessante usar estruturas na Stack no lugar da Heap. Isso alivia o GC, pois o Stack não é gerenciado pelo Garbage Collector. Infelizmente, essa solução exige adoção de códigos unsafe.
public static unsafe string StackAllocReverseString(string input)
{
var reversedCharArray = stackalloc char[input.Length];
for (
int i = input.Length - 1, destIndex = 0;
i >= 0;
i--, destIndex++
)
{
reversedCharArray[destIndex] = input[i];
}
return new string(reversedCharArray);
}
Na primeira linha, uso stackalloc para alocar o array de caracteres na stack no lugar de usar a heap. Entrentanto, tome cuidado com essa abordagem. Lembre-se que StackOverflowException é uma das exceptions que não podem ser tratadas e que encerram o processo.
Ponteiros! Unsafe.
Códigos marcados como unsafe costumam assustar programadores menos experientes. Geralmente, esse medo não é justificado. Vejamos:
public static unsafe string UnsafeReverseString(string input)
{
var result = new string(' ', input.Length);
fixed (char* source = input)
fixed (char* dest = result)
{
for (
int i = input.Length - 1, destIndex = 0;
i >= 0;
i--, destIndex++
)
{
dest[destIndex] = source[i];
}
}
return result;
}
Aqui, temos agora apenas a alocação da string que será gerada como retorno. É a versão com menor “peso” para o GC e sem riscos para a Stack.
Teoricamente, essa seria a nossa versão mais rápida.
Usando Span<T>
.NET agora oferece uma forma nova e excitante de manipular memória com menos alocações. Trata-se da classe Span<T>. Vejamos:
public static string SpanReverseString(string input) =>
string.Create(input.Length, input, (span, s) =>
{
var index = 0;
for (var i = s.Length - 1; i >= 0; i--)
{
span[index] = s[i];
index++;
}
});
A vantagem desse código é que ele também não gera alocações desnecessárias e… não é unsafe.
Comparando performance
Usei BenchmarkDotNet para fazer a comparação de performance dessas versões. Aqui está o resultado:
Method | Mean | Error | StdDev |
------------------------ |----------:|----------:|-----------:|
LinqReverseString | 506.18 ns | 9.8646 ns | 13.8288 ns |
CharReverseString | 40.80 ns | 0.8386 ns | 1.4466 ns |
StackAllocReverseString | 39.00 ns | 0.7961 ns | 1.1160 ns |
UnsafeReverseString | 27.97 ns | 0.5840 ns | 0.9092 ns |
SpanReverseString | 33.40 ns | 0.7612 ns | 1.0670 ns |
Como você pode constatar, chegamos a uma melhoria de 18x (nada mal). O que fizemos foi “reconhecer e respeitar” o GC.
Concluindo
Seguramente, inversão de strings não é a grande “ofensora” do que trata de performance para sua aplicação. Meu ponto é que o desenvolvedor costuma ignorar o GC em todo o código e, a soma de pequenos prejuízos resulta uma aplicação lenta.
Seguramente, a versão com LINQ é a mais declarativa e, seguramente, a mais expressiva. Porém, ela também é a mais lenta. Temos que considerar esse tradeoff na hora de escrever nosso código.
Por fim, temos recursos novos para melhorar a performance de nossas aplicações. A classe Span pode representar uma revolução permitindo que escrevamos códigos muito mais rápidos (e economicos) sem ter de recorrer a blocos unsafe. Recomendo que você a estude com carinho.