Kiedy używać StringBuilder?

83

Rozumiem zalety StringBuilder.

Ale jeśli chcę połączyć 2 ciągi, to zakładam, że lepiej (szybciej) to zrobić bez StringBuildera. Czy to jest poprawne?

W którym momencie (liczba ciągów) lepiej jest używać StringBuilder?

Shiraz Bhaiji
źródło
1
Myślę, że zostało to omówione wcześniej.
Mark Schultheiss

Odpowiedzi:

79

Gorąco zachęcam do przeczytania Smutnej tragedii teatru mikro-optymalizacji autorstwa Jeffa Atwooda.

Traktuje prostą konkatenację, StringBuilder i inne metody.

Teraz, jeśli chcesz zobaczyć liczby i wykresy, kliknij link;)

Alex Bagnolini
źródło
+1 za tak ! Czas spędzony na martwieniu się o to to czas spędzony na nie robieniu czegoś, co może mieć znaczenie.
Greg D
8
Twój odczyt jest jednak zły: nie ma to znaczenia w wielu przypadkach, gdy nie ma pętli, w innych przypadkach może to mieć znaczenie DUŻO
Peter
1
Usunąłem zmianę, ponieważ była to tylko błędna informacja w zaakceptowanej odpowiedzi.
Peter
2
i żeby pokazać, jakie to ma znaczenie, z artykułu, do którego się odnosisz: „W większości języków, w których zbierane są elementy bezużyteczne, ciągi znaków są niezmienne: po dodaniu dwóch ciągów zawartość obu jest kopiowana. W miarę dodawania wyników w tej pętli , coraz więcej pamięci jest przydzielanych za każdym razem. Prowadzi to bezpośrednio do okropnego quadradic n2 ”
Peter
2
Dlaczego jest to akceptowana odpowiedź. Nie sądzę, żeby po prostu upuszczenie linku i powiedzenie „przeczytaj to” to dobra odpowiedź
Kolob Canyon,
45

Ale jeśli chcę połączyć 2 ciągi, to zakładam, że lepiej (szybciej) zrobić to bez StringBuildera. Czy to jest poprawne?

To prawda, możesz znaleźć, dlaczego dokładnie wyjaśniono bardzo dobrze:

http://www.yoda.arachsys.com/csharp/stringbuilder.html

Podsumowując: jeśli możesz połączyć struny za jednym razem, np

var result = a + " " + b  + " " + c + ..

lepiej jest bez StringBuilder, ponieważ wykonywana jest tylko kopia (długość otrzymanego ciągu jest obliczana wcześniej);

Dla struktury takiej jak

var result = a;
result  += " ";
result  += b;
result  += " ";
result  += c;
..

za każdym razem tworzone są nowe obiekty, więc warto rozważyć StringBuilder.

Na koniec artykuł podsumowuje te praktyczne zasady:

Reguły kciuka

Kiedy więc należy używać StringBuilder, a kiedy operatorów konkatenacji ciągów?

  • Zdecydowanie używaj StringBuilder, gdy konkatenujesz w nietrywialnej pętli - zwłaszcza jeśli nie wiesz na pewno (w czasie kompilacji), ile iteracji wykonasz w pętli. Na przykład, czytanie pliku po znaku na raz, budowanie ciągu znaków podczas używania operatora + = jest potencjalnie samobójstwem.

  • Zdecydowanie używaj operatora konkatenacji, kiedy możesz (czytelnie) określić wszystko, co ma być połączone w jednej instrukcji. (Jeśli masz tablicę elementów do konkatenacji, rozważ jawne wywołanie String.Concat - lub String.Join, jeśli potrzebujesz separatora).

  • Nie bój się rozbijać literałów na kilka połączonych bitów - wynik będzie taki sam. Możesz zwiększyć czytelność, na przykład dzieląc długi literał na kilka wierszy bez szkody dla wydajności.

  • Jeśli potrzebujesz pośrednich wyników konkatenacji dla czegoś innego niż podawanie następnej iteracji konkatenacji, StringBuilder nie pomoże. Na przykład, jeśli utworzysz pełne imię i nazwisko z imienia i nazwiska, a następnie dodasz trzecią informację (może pseudonim) na końcu, skorzystasz z StringBuilder tylko wtedy, gdy tego nie zrobisz potrzebujesz ciągu (imię + nazwisko) do innych celów (tak jak w przykładzie, który tworzy obiekt Person).

  • Jeśli masz do zrobienia tylko kilka konkatenacji i naprawdę chcesz je wykonać w osobnych instrukcjach, nie ma znaczenia, którą drogą pójdziesz. To, który sposób jest bardziej efektywny, będzie zależeć od liczby konkatenacji, rozmiarów łańcucha i kolejności, w jakiej są one konkatenowane. Jeśli naprawdę uważasz, że ten fragment kodu jest wąskim gardłem wydajności, profiluj go lub testuj w obie strony.

Piotr
źródło
14

System.String jest niezmiennym obiektem - oznacza to, że za każdym razem, gdy zmodyfikujesz jego zawartość, przydzieli nowy ciąg, a to wymaga czasu (i pamięci?). Używając StringBuilder, modyfikujesz rzeczywistą zawartość obiektu bez przydzielania nowej.

Dlatego używaj StringBuilder, gdy musisz wykonać wiele modyfikacji ciągu.

Victor Hurdugaci
źródło
8

Niezupełnie ... powinieneś użyć StringBuilder, jeśli łączysz duże ciągi lub masz wiele konkatenacji, jak w pętli.

Konstabl
źródło
1
To jest złe. Należy używać StringBuildertylko wtedy, gdy pętla lub konkatenacja stanowi problem ze specyfikacją.
Alex Bagnolini
2
@Alex: Czy nie zawsze tak jest? ;) Nie, serio, zawsze używałem StringBuilder do konkatenacji wewnątrz pętli ... chociaż wszystkie moje pętle mają więcej niż 1k iteracji ... @Binary: Normalnie powinno to być skompilowane string s = "abcd", przynajmniej to ostatnia rzecz Słyszałem ... chociaż ze zmiennymi byłby to najprawdopodobniej Concat.
Bobby
1
Fakt: prawie ZAWSZE NIE JEST. Zawsze używałem operatorów łańcuchowych a + "hello" + "somethingelse"i nigdy nie musiałem się tym martwić. Jeśli stanie się to problemem, użyję StringBuilder. Ale przede wszystkim nie martwiłem się tym i spędzałem mniej czasu na pisaniu.
Alex Bagnolini
3
W przypadku dużych ciągów nie ma absolutnie żadnej korzyści z wydajności - tylko w przypadku wielu konkatenacji.
Konrad Rudolph
1
@Konrad: Czy na pewno nie ma korzyści z wydajności? Za każdym razem, gdy łączysz duże ciągi, kopiujesz dużą ilość danych; Za każdym razem, gdy łączysz małe ciągi, kopiujesz tylko niewielką ilość danych.
LukeH
6
  • Jeśli łączysz ciągi w pętli, powinieneś rozważyć użycie StringBuilder zamiast zwykłego String
  • Jeśli jest to pojedyncza konkatenacja, możesz w ogóle nie zauważyć różnicy w czasie wykonywania

Oto prosta aplikacja testowa, aby to udowodnić:

class Program
{
    static void Main(string[] args)
    {
        const int testLength = 30000;
        var StartTime = DateTime.Now;

        //TEST 1 - String
        StartTime = DateTime.Now;
        String tString = "test string";
        for (int i = 0; i < testLength; i++)
        {
            tString += i.ToString();
        }
        Console.WriteLine((DateTime.Now - StartTime).TotalMilliseconds.ToString());
        //result: 2000 ms

        //TEST 2 - StringBuilder
        StartTime = DateTime.Now;
        StringBuilder tSB = new StringBuilder("test string");
        for (int i = 0; i < testLength; i++)
        {
            tSB.Append(i.ToString());
        }
        Console.WriteLine((DateTime.Now - StartTime).TotalMilliseconds.ToString());
        //result: 4 ms

        Console.ReadLine();
    }
}

Wyniki:

  • 30 000 iteracji

    • Ciąg - 2000 ms
    • StringBuilder - 4 ms
  • 1000 iteracji

    • Ciąg - 2 ms
    • StringBuilder - 1 ms
  • 500 iteracji

    • Ciąg - 0 ms
    • StringBuilder - 0 ms
adrian.krzysztofek
źródło
5

Parafrazować

Wtedy policzysz do trzech, nie więcej, nie mniej. Trzy to liczba, którą policzysz, a liczba do obliczenia to trzy. Nie policzysz czterech ani dwóch, chyba że przejdziesz do trzech. Gdy zostanie osiągnięta liczba trzy, która jest trzecią liczbą, lobbuj swój Święty Granat Ręczny z Antiochii

Generalnie używam konstruktora ciągów dla dowolnego bloku kodu, który spowodowałby konkatenację trzech lub więcej ciągów.

Russell Steen
źródło
To zależy: Concetanation tworzy tylko jedną kopię: "Russell" + "" + Steen + ".", Zrobi tylko jedną kopię, ponieważ wcześniej oblicza długość struny. Dopiero gdy musisz podzielić konkatenację, powinieneś zacząć myśleć o budowniczym
Peter
4

Nie ma ostatecznej odpowiedzi, tylko praktyczne zasady. Moje własne zasady wyglądają mniej więcej tak:

  • Jeśli łączysz się w pętli, zawsze używaj StringBuilder.
  • Jeśli ciągi są duże, zawsze używaj pliku StringBuilder.
  • Jeśli kod konkatenacji jest uporządkowany i czytelny na ekranie, prawdopodobnie jest w porządku.
    Jeśli tak nie jest, użyj pliku StringBuilder.
LukeH
źródło
Wiem, że to stary temat, ale znam się tylko na uczeniu się i chciałem wiedzieć, co uważasz za „duży ciąg”?
MatthewD
4

Ale jeśli chcę połączyć 2 ciągi, zakładam, że lepiej i szybciej jest to zrobić bez StringBuilder. Czy to jest poprawne?

Tak. Ale co ważniejsze, użycie wanilii w takich sytuacjach jest znacznie bardziej czytelneString . Z drugiej strony używanie go w pętli ma sens i może być tak czytelne, jak konkatenacja.

Obchodziłbym się z praktycznymi regułami, które jako próg cytują określoną liczbę konkatenacji. Używanie go w pętlach (i tylko w pętlach) jest prawdopodobnie równie przydatne, łatwiejsze do zapamiętania i ma więcej sensu.

Konrad Rudolph
źródło
„Byłbym ostrożny wobec praktycznych reguł, które jako próg cytują określoną liczbę konkatenacji” <this. Po zastosowaniu zdrowego rozsądku pomyśl o osobie, która wróci do Twojego kodu za 6 miesięcy.
Phil Cooper,
4

Ponieważ trudno jest znaleźć wytłumaczenie tego, które nie ma wpływu ani na opinie, ani na walkę o dumę, pomyślałem, że napiszę trochę kodu na LINQpad, aby samemu to przetestować.

Zauważyłem, że używanie łańcuchów o małych rozmiarach zamiast i.ToString () zmienia czasy odpowiedzi (widoczne w małych pętlach).

Test wykorzystuje różne sekwencje iteracji, aby utrzymać pomiary czasu w rozsądnie porównywalnych zakresach.

Skopiuję kod na końcu, abyś mógł spróbować samemu (wyniki.Charts ... Dump () nie będzie działać poza LINQPad).

Dane wyjściowe (oś X: liczba przetestowanych iteracji, oś Y: czas mierzony w taktach):

Sekwencja iteracji: 2, 3, 4, 5, 6, 7, 8, 9, 10 Sekwencja iteracji: 2, 3, 4, 5, 6, 7, 8, 9, 10

Sekwencja iteracji: 10, 20, 30, 40, 50, 60, 70, 80 Sekwencja iteracji: 10, 20, 30, 40, 50, 60, 70, 80

Sekwencja iteracji: 100, 200, 300, 400, 500 Sekwencja iteracji: 100, 200, 300, 400, 500

Kod (napisany przy użyciu LINQPad 5):

void Main()
{
    Test(2, 3, 4, 5, 6, 7, 8, 9, 10);
    Test(10, 20, 30, 40, 50, 60, 70, 80);
    Test(100, 200, 300, 400, 500);
}

void Test(params int[] iterationsCounts)
{
    $"Iterations sequence: {string.Join(", ", iterationsCounts)}".Dump();

    int testStringLength = 10;
    RandomStringGenerator.Setup(testStringLength);
    var sw = new System.Diagnostics.Stopwatch();
    var results = new Dictionary<int, TimeSpan[]>();

    // This call before starting to measure time removes initial overhead from first measurement
    RandomStringGenerator.GetRandomString(); 

    foreach (var iterationsCount in iterationsCounts)
    {
        TimeSpan elapsedForString, elapsedForSb;

        // string
        sw.Restart();
        var str = string.Empty;

        for (int i = 0; i < iterationsCount; i++)
        {
            str += RandomStringGenerator.GetRandomString();
        }

        sw.Stop();
        elapsedForString = sw.Elapsed;


        // string builder
        sw.Restart();
        var sb = new StringBuilder(string.Empty);

        for (int i = 0; i < iterationsCount; i++)
        {
            sb.Append(RandomStringGenerator.GetRandomString());
        }

        sw.Stop();
        elapsedForSb = sw.Elapsed;

        results.Add(iterationsCount, new TimeSpan[] { elapsedForString, elapsedForSb });
    }


    // Results
    results.Chart(r => r.Key)
    .AddYSeries(r => r.Value[0].Ticks, LINQPad.Util.SeriesType.Line, "String")
    .AddYSeries(r => r.Value[1].Ticks, LINQPad.Util.SeriesType.Line, "String Builder")
    .DumpInline();
}

static class RandomStringGenerator
{
    static Random r;
    static string[] strings;

    public static void Setup(int testStringLength)
    {
        r = new Random(DateTime.Now.Millisecond);

        strings = new string[10];
        for (int i = 0; i < strings.Length; i++)
        {
            strings[i] = Guid.NewGuid().ToString().Substring(0, testStringLength);
        }
    }

    public static string GetRandomString()
    {
        var indx = r.Next(0, strings.Length);
        return strings[indx];
    }
}
Menol
źródło
3

Dopóki możesz fizycznie wpisać liczbę konkatenacji (a + b + c ...), nie powinno to robić dużej różnicy. N do kwadratu (przy N = 10) to 100-krotne spowolnienie, co nie powinno być takie złe.

Duży problem występuje, gdy łączysz setki łańcuchów. Przy N = 100 uzyskujesz 10000-krotne spowolnienie. Co jest dość złe.

wisty
źródło
2

Nie sądzę, aby istniała cienka granica między tym, kiedy używać, a kiedy nie. Chyba że ktoś przeprowadził obszerne testy, aby wyjść ze złotymi warunkami.

Dla mnie nie użyję StringBuilder, jeśli tylko połączę 2 ogromne ciągi. Jeśli istnieje pętla z nieokreśloną liczbą, prawdopodobnie tak będzie, nawet jeśli pętla może być niewielka.

okw
źródło
Rzeczywiście byłoby całkowicie błędne, gdybyśmy wykorzystali StringBuilder do konkatacji 2 ciągów, ale nie ma to nic wspólnego z perf. testowanie - to po prostu używanie go do niewłaściwych celów.
Marc Gravell
1

Pojedyncza konkatenacja nie jest warta używania StringBuilder. Zwykle stosuję 5 konkatenacji jako praktyczną zasadę.

Matt Wrock
źródło