DEV Community

Roberto Higor
Roberto Higor

Posted on

Porque você precisa conhecer o StringBuilder

Introdução

Nesse artigo vou explicar as vantagens e diferenças entre a classe StringBuilder e a concatenação de strings com "+". Por ser uma ferramenta muito utilizada no dia a dia, é importante entender as diferenças de como as duas funcionam internamente.

Mutável vs Imutável

A classe String é do tipo imutável, ou seja, após criado seu valor não pode ser alterado. Apesar de parecer que ao adicionar uma string a outra existente, estamos apenas incrementando a variável anterior com mais valores, na realidade é alocado e criado uma nova variável na memória contendo a junção das duas strings, por exemplo:

string textoOriginal = "Olá, ";
string mundo = "Mundo";
// Internamente está sendo criado uma nova string e 
// alocado na variável já existente.
textoOriginal += mundo;
Enter fullscreen mode Exit fullscreen mode

Internamente, o compilador converte a operação "+" em String.Concat

Para processos onde é realizado manipulações em string em um alto volume, isso pode resultar em um impacto tanto na performance quanto no uso de memória, por ser necessário uma nova alocação a cada concatenação, resultando também em chamadas excessivas ao Garbage Collector.

Para casos onde precisamos de uma variável mutável, a alternativa é a classe StringBuilder que, diferente da String, trabalha com dados mutáveis. Ou seja, uma instância do StringBuilder pode ser modificada (incrementadas, substituidas etc) sem precisar criar uma nova instância. Essa é sua principal vantagem, que é feito através do uso de um buffer interno, no qual permite com que o valor da string aumente e diminua.

Buffer

Diferente da String, o StringBuilder trabalha com um buffer para acomodar as modificações. Caso o buffer se esgote, será alocado um buffer maior, copiando os dados do buffer antigo. A alocação padrão do StringBuilder comporta 16 caracteres, tendo o máximo o valor da constante Int32.MaxValue, que ao atigindo, resulta em um OutOfMemoryException.

Para reduzir a frequência no qual é alocado mais memória para o buffer interno, pode-se utilizar o construtor que recebe o tamanho da memória no qual queremos alocar (em caracteres)

// Inicia stringbuilder com tamanho para 100 caracteres
var sb = new StringBuilder(100)
Enter fullscreen mode Exit fullscreen mode

É dito que uma alocação exata pode trazer uma melhorias na performance e redução na alocação de memória, no entanto nos exemplos realizados a versão com Buffer após um certo volume de dados trouxe performance pior do que a versão sem buffer.

  [Benchmark]
    public string StringBuilderLoop1kComBufferTest()
    {
        StringBuilder stringBuilder = new("Olá, ", 1000 * "Mundo".Length);
        for (int i = 0; i < 1000; i++)
            stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }

    [Benchmark]
    public string StringBuilderLoop1kSemBufferTest()
    {
        StringBuilder stringBuilder = new("Olá, ");
        for (int i = 0; i < 1000; i++)
            stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }

        [Benchmark]
    public string StringBuilderLoop10kComBufferTest()
    {
        StringBuilder stringBuilder = new("Olá, ", 10000 * "Mundo".Length);
        for (int i = 0; i < 10000; i++)
            stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }

    [Benchmark]
    public string StringBuilderLoop10kSemBufferTest()
    {
        StringBuilder stringBuilder = new("Olá, ");
        for (int i = 0; i < 10000; i++)
            stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }
Enter fullscreen mode Exit fullscreen mode
Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
StringBuilderLoop1kComBufferTest 3.846 us 0.0176 us 0.0156 us 1.8005 0.1144 - 29.47 KB
StringBuilderLoop1kSemBufferTest 4.383 us 0.0256 us 0.0214 us 1.6174 0.0916 - 26.5 KB
StringBuilderLoop10kComBufferTest 94.036 us 0.4714 us 0.4410 us 62.3779 62.3779 62.3779 211.13 KB
StringBuilderLoop10kSemBufferTest 70.643 us 0.9460 us 0.8386 us 31.1279 31.1279 31.1279 208.57 KB

Para a execução do método Append() mil vezes, a versão com buffer teve uma performance um pouco melhor. No entanto, para a versão de 10 mil vezes, a performance foi consideravelmente pior.

AppendJoin vs Join

Ambas as classes possuem também métodos de Join, sendo StringBuilder.AppendJoin e String.Join. Para casos onde temos um array de strings, o string.Concat pode ser uma alternativa melhor por conseguir determinar o tamanho final da string ao examinar o array.

Em casos onde é necessário chamar várias vezes (como em um loop), o AppendJoin consegue se sair melhor pela vantagem da imutabilidade.

O StringBuilder.AppendJoin é uma combinação entre o String.Join e o StringBuilder.Append, por chamar o Append internamente. Para cada valor, também é chamado o ToString(), o que pode ser ruim para valores que não são string (como int e decimal).

A comparação pode ser vista no benchmark abaixo:

private static string[] _dataList = 
    System.IO.File.ReadAllLines(@"randomString200-1.txt");

    [Benchmark]
    public string StringBuilderJoin()
    {
        StringBuilder stringBuilder = new();
        stringBuilder.AppendJoin("", _dataList);
        return stringBuilder.ToString();
    }

    [Benchmark]
    public string StringJoin()
    {
        return String.Join("", _dataList);
    }

     [Benchmark]
    public string StringConcat()
    {
        return String.Concat(_dataList);
    }
Enter fullscreen mode Exit fullscreen mode
Method Mean Error StdDev Gen0 Gen1 Allocated
StringBuilderJoin 4.041 us 0.0410 us 0.0383 us 5.1193 0.8469 83.71 KB
StringJoin 2.363 us 0.0460 us 0.0644 us 2.0523 - 33.69 KB
StringConcat 1.686 us 0.0127 us 0.0119 us 2.0523 - 33.69 KB

Nesse caso o String.Concat teve uma performance melhor (pelo fato do exemplo utilizar um array), além de alocar menos memoria do que o StringBuilder.Join. Em seguida vem o StringJoin.

Para os cenários de Array com tamanho fixo, o próprio String.Concat consegue se sair melhor.

Quando utilizar

O StringBuilder é recomendado quando se utiliza um alto volume de concatenações de string, como por exemplo em um loop. Segundo a microsoft, a classe String deve ser utilizada em casos de:

  • Quando o número de modificações na string for pequeno;
  • Quando é realizado um número fixo de concatenações, pois nesse caso o compilador pode combinar em uma única operação;
  • Quando é necessário utilizar métodos de pesquisas como IndesOf ou StartsWith, pelo fato do StringBuilder não suportar sem antes o converter em String, o que negaria seus benefícios.

Já para o caso de uso do StringBuilder:

  • Quando o código pode fazer um número desconhecido de modificações (como em loops);
  • Quando o código fará um número significativo de alterações em uma string.

Ao realizar a concatenação de uma palavra 100 mil vezes, a vantagem do StringBuilder fica bem evidente:

   [Benchmark]
    public string StringBuilderLoopTest()
    {
        StringBuilder stringBuilder = new("Olá, ");
        for (int i = 0; i < 100000; i++)
            stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }

    [Benchmark]
    public string StringConcatLoopTest()
    {
        string textoOriginal = "Olá, ";
        string mundo = "Mundo";
        for (int i = 0; i < 100000; i++)
            textoOriginal += mundo;
        return textoOriginal;
    }
Enter fullscreen mode Exit fullscreen mode
Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
StringBuilderTest 18.642 ns 0.3487 ns 0.3261 ns 0.0091 - - 152 B
StringConcatTest 8.216 ns 0.0610 ns 0.0571 ns 0.0029 - - 48 B
StringBuilderLoopTest 699,651.201 ns 2,971.4637 ns 2,779.5089 ns 249.0234 249.0234 249.0234 2013684 B
StringConcatLoopTest 5,013,108,142.857 ns 24,203,777.6872 ns 21,456,018.4848 ns 11800000.0000 11779000.0000 11776000.0000 50007779072 B

Além do menor tempo de execução e memória alocada, com o StringBuilder houve menos chamadas ao Garbage Collector Gen 0, devido ao fato da concatenação alocar mais memória ao criar novos objetos de string, resultando em mais chamadas ao GC Generation 0.

Quando não utilizar

Para strings pequenas, o StringBuilder pode não trazer um ganho muito significativo de memória, além de que utilizar o "+=" nesses casos possuí uma sintaxe mais simples e rápida de se desenvolver.

Outro caso é quando já existe um array de strings de tamanho definido, como citado no capítulo sobre Join, em que o String.Concat consegue determinar de uma vez o quanto é preciso alocado.

Exemplo de quando não utilizar:

using System.Text;
using BenchmarkDotNet.Attributes;

namespace Domain.Benchmark;
[MemoryDiagnoser]
public class StringBuilderBenchmark
{
    [Benchmark]
    public string StringBuilderTest()
    {
        `StringBuilder` stringBuilder = new("Olá, ");
        stringBuilder.Append("Mundo");
        return stringBuilder.ToString();
    }

    [Benchmark]
    public string StringConcatTest()
    {
        string textoOriginal = "Olá, ";
        string mundo = "Mundo";
        return textoOriginal + mundo;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse caso, o StringBuilder além de ter sido mais lento, alocou mais memória do que a concatenação normal.

Method Mean Error StdDev Gen0 Allocated
StringBuilderTest 18.626 ns 0.1947 ns 0.1726 ns 0.0091 152 B
StringConcatTest 8.320 ns 0.0479 ns 0.0425 ns 0.0029 48 B

Conclusão

Esse artigo buscou mostrar de forma breve as vantagens e quando utilizar a classe StringBuilder, sendo uma grande aliada na performance de sistemas que executem um grande volume de concatenações de string. Seu uso deve ser mais para casos onde não sabemos o tamanho da string, pela sua propriedade de ser mutável. Em casos em que sabemos o tamanho, como ao juntar um array de strings, o string.Concat consegue uma vantagem.

Seguem os resultados dos benchmarks com tamanhos de string variados:

Method Mean Error StdDev Rank Gen0 Gen1 Gen2 Allocated
StringBuilderTest 19.107 ns 0.2487 ns 0.2205 ns 2 0.0091 - - 152 B
StringConcatTest 8.414 ns 0.1637 ns 0.1531 ns 1 0.0029 - - 48 B
StringBuilderLoop1kTest 4,503.729 ns 85.7993 ns 80.2567 ns 3 1.6174 0.0916 - 27136 B
StringConcatLoop1kTest 161,308.281 ns 3,194.8901 ns 4,154.2595 ns 5 301.2695 7.5684 - 5040000 B
StringBuilderLoop10kTest 70,641.925 ns 698.0930 ns 652.9966 ns 4 31.1279 31.1279 31.1279 213579 B
StringConcatLoop10kTest 20,383,109.233 ns 400,692.3340 ns 492,086.1706 ns 7 64593.7500 46625.0000 43625.0000 500414677 B
StringBuilderLoop100kTest 709,105.225 ns 7,222.7404 ns 6,402.7713 ns 6 249.0234 249.0234 249.0234 2013684 B
StringConcatLoop100kTest 5,111,168,506.667 ns 76,749,055.3313 ns 71,791,113.9755 ns 8 11783000.0000 11762000.0000 11759000.0000 50007763560 B

Fonte

https://www.stevejgordon.co.uk/creating-strings-with-no-allocation-overhead-using-string-create-csharp
https://learn.microsoft.com/pt-br/dotnet/api/system.text.stringbuilder?view=net-7.0#the-string-and-stringbuilder-types
https://medium.com/c-sharp-progarmming/stringbuilder-vs-concatenation-in-net-b817ccec331e
https://www.macoratti.net/21/11/c_benchmark1.htm
https://www.oreilly.com/library/view/c-cookbook/0596003390/ch02s22.html
https://www.infoworld.com/article/3616600/when-to-use-string-vs-stringbuilder-in-net-core.html
https://davecallan.com/improve-performance-stringbuilder-dotnet-setting-capacity/
https://andrewlock.net/a-deep-dive-on-stringbuilder-part-2-appending-strings-built-in-types-and-lists/

Top comments (0)