Czy String.Format jest tak wydajny jak StringBuilder


160

Załóżmy, że mam program do tworzenia ciągów w C #, który robi to:

StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();

czy byłoby to tak wydajne czy bardziej wydajne, jak posiadanie:

string cat = "cat";
string s = String.Format("The {0} in the hat", cat);

Jeśli tak, dlaczego?

EDYTOWAĆ

Po kilku interesujących odpowiedziach zdałem sobie sprawę, że prawdopodobnie powinienem był trochę bardziej wyrazić, o co pytałem. Nie pytałem o to, co jest szybsze w konkatenacji ciągu, ale które jest szybsze w wstrzykiwaniu jednego ciągu do drugiego.

W obu powyższych przypadkach chcę wstrzyknąć jeden lub więcej ciągów w środek wstępnie zdefiniowanego ciągu szablonu.

Przepraszam za zamieszanie


Zostaw te otwarte, aby umożliwić przyszłe ulepszenia.
Mark Biek

4
W specjalnym scenariuszu najszybszy nie jest żaden z tych: jeśli część do wymiany ma taki sam rozmiar jak nowa część, można zmienić ciąg na miejscu. Niestety wymaga to odbicia lub niebezpiecznego kodu i celowo narusza niezmienność ciągu. Nie jest to dobra praktyka, ale jeśli liczy się szybkość ... :)
Abel

w przykładzie podanym powyżej string s = "The "+cat+" in the hat";może być najszybszy, chyba że zostanie użyty w pętli, w którym to przypadku najszybszy będzie z StringBuilder zainicjalizowanym poza pętlą.
Surya Pratap

Odpowiedzi:


146

UWAGA: Ta odpowiedź została napisana, gdy bieżącą wersją był .NET 2.0. Może to już nie mieć zastosowania do późniejszych wersji.

String.Formatużywa StringBuilderwewnętrznie:

public static string Format(IFormatProvider provider, string format, params object[] args)
{
    if ((format == null) || (args == null))
    {
        throw new ArgumentNullException((format == null) ? "format" : "args");
    }

    StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
    builder.AppendFormat(provider, format, args);
    return builder.ToString();
}

Powyższy kod jest fragmentem z mscorlib, więc pytanie brzmi „jest StringBuilder.Append()szybsze niż StringBuilder.AppendFormat()”?

Bez testów porównawczych prawdopodobnie powiedziałbym, że powyższy przykład kodu działałby szybciej przy użyciu .Append(). Ale to przypuszczenie, spróbuj wykonać testy porównawcze i / lub profilowanie tych dwóch, aby uzyskać właściwe porównanie.

Ten facet, Jerry Dixon, przeprowadził testy porównawcze:

http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

Zaktualizowano:

Niestety powyższy link od tamtej pory umarł. Jednak wciąż jest kopia w Way Back Machine:

http://web.archive.org/web/20090417100252/http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

Ostatecznie zależy to od tego, czy formatowanie ciągu będzie wywoływane powtarzalnie, tj. Wykonujesz poważne przetwarzanie tekstu ponad 100 megabajtów tekstu, czy też jest wywoływane, gdy użytkownik od czasu do czasu klika przycisk. Jeśli nie wykonujesz jakiejś ogromnej pracy przetwarzania wsadowego, trzymałbym się String.Format, pomaga to w czytelności kodu. Jeśli podejrzewasz wąskie gardło perf, przyklej profiler do swojego kodu i zobacz, gdzie on naprawdę jest.


8
Jeden problem z benchmarków na stronie Jerry Dixon jest to, że nigdy nie zwraca .ToString()na StringBuilderobiekcie. W wielu iteracjach ten czas robi wielką różnicę i oznacza, że ​​nie porównuje on jabłek do jabłek. To jest powód, dla którego pokazuje tak świetne wyniki StringBuilderi prawdopodobnie jest przyczyną jego zdziwienia. Właśnie powtarzane benchmark sprostowanie tego błędu i dostał Oczekiwane wyniki: String +operator był najszybszy, a następnie StringBuilder, z String.Formatwychowywaniem tyłu.
Ben Collins,

5
6 lat później już tak nie jest. W Net4 string.Format () tworzy i buforuje instancję StringBuilder, której używa ponownie, więc w niektórych przypadkach testowych może być szybsza niż StringBuilder. W odpowiedzi umieściłem poprawiony test porównawczy (który nadal mówi, że concat jest najszybszy, a dla mojego przypadku testowego format jest o 10% wolniejszy niż StringBuilder).
Chris F Carroll

45

Z dokumentacji MSDN :

Wydajność operacji konkatenacji dla obiektu String lub StringBuilder zależy od tego, jak często występuje alokacja pamięci. Operacja konkatenacji String zawsze przydziela pamięć, podczas gdy operacja konkatenacji StringBuilder przydziela pamięć tylko wtedy, gdy bufor obiektu StringBuilder jest zbyt mały, aby pomieścić nowe dane. W związku z tym klasa String jest preferowana do operacji konkatenacji, jeśli łączona jest stała liczba obiektów String. W takim przypadku poszczególne operacje konkatenacji mogą nawet zostać połączone w jedną operację przez kompilator. Obiekt StringBuilder jest preferowany dla operacji konkatenacji, jeśli konkatenowana jest dowolna liczba ciągów; na przykład, jeśli pętla łączy losową liczbę ciągów danych wejściowych użytkownika.


12

Przeprowadziłem kilka szybkich testów wydajności i dla 100 000 operacji uśrednionych w 10 przebiegach pierwsza metoda (String Builder) zajmuje prawie połowę czasu drugiej (String Format).

Więc jeśli jest to rzadkie, nie ma to znaczenia. Ale jeśli jest to typowa operacja, możesz użyć pierwszej metody.


10

Spodziewałbym się, że String.Format będzie wolniejszy - musi przeanalizować ciąg, a następnie go połączyć .

Kilka uwag:

  • Format to sposób na znalezienie ciągów znaków widocznych dla użytkownika w profesjonalnych aplikacjach; pozwala to uniknąć błędów lokalizacyjnych
  • Jeśli znasz wcześniej długość wynikowego ciągu, użyj konstruktora StringBuilder (Int32), aby wstępnie zdefiniować pojemność

8

Myślę, że w większości przypadków taka przejrzystość, a nie wydajność, powinna być Twoim największym zmartwieniem. Jeśli nie zmiażdżysz ton strun lub nie zbudujesz czegoś dla urządzenia mobilnego o niższej mocy, prawdopodobnie nie wpłynie to zbytnio na prędkość biegu.

Zauważyłem, że w przypadkach, w których buduję ciągi znaków w sposób dość liniowy, najlepszym rozwiązaniem jest wykonywanie prostych konkatenacji lub użycie StringBuilder. Sugeruję to w przypadkach, gdy większość tworzonego ciągu znaków jest dynamiczna. Ponieważ bardzo niewielka część tekstu jest statyczna, najważniejsze jest to, aby było jasne, gdzie jest umieszczany każdy fragment tekstu dynamicznego na wypadek, gdyby wymagał aktualizacji w przyszłości.

Z drugiej strony, jeśli mówisz o dużym kawałku statycznego tekstu z dwiema lub trzema zmiennymi, nawet jeśli jest to trochę mniej wydajne, myślę, że jasność, jaką zyskujesz dzięki łańcuchowi, jest tego warta. Użyłem tego na początku tego tygodnia, kiedy musiałem umieścić jeden bit dynamicznego tekstu w środku 4-stronicowego dokumentu. Łatwiej będzie zaktualizować ten duży fragment tekstu, jeśli jest on w jednym kawałku, niż aktualizować trzy elementy, które łączysz razem.


Tak! Użyj String.Format, gdy ma to sens, np. Kiedy formatujesz łańcuchy. Użyj konkatenacji ciągów lub StringBuilder podczas wykonywania konkatenacji mechanicznej. Zawsze staraj się wybrać metodę, która przekazuje twoje zamiary następnemu opiekunowi.
Rob

8

Choćby dlatego, że string.Format nie robi dokładnie tego, co myślisz, oto powtórka testów 6 lat później na Net45.

Concat jest nadal najszybszy, ale tak naprawdę to mniej niż 30% różnicy. StringBuilder i Format różnią się zaledwie o 5–10%. Kilka razy wykonałem testy w zakresie 20%.

Milisekundy, milion iteracji:

  • Konkatenacja: 367
  • Nowy stringBuilder dla każdego klucza: 452
  • Cached StringBuilder: 419
  • string.Format: 475

Lekcja, którą wyciągam, jest taka, że ​​różnica w wydajności jest trywialna i nie powinna powstrzymywać Cię przed pisaniem najprostszego czytelnego kodu, jaki możesz. Co za moje pieniądze jest często, ale nie zawsze a + b + c.

const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);

var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];

var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();

for(int i=0; i<iterations; i++){
    var key1= keyprefix+":" + i.ToString();
    concatkeys[i]=key1;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
    stringbuilderkeys[i]= key2;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
    cachedsbkeys[i]= key2b;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("string.Format");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
    formatkeys[i]= key3;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);

2
Mówiąc „string.Format nie robi dokładnie tego, co myślisz” mam na myśli, że w kodzie źródłowym 4.5 próbuje utworzyć i ponownie użyć buforowanej instancji StringBuilder. Więc włączyłem to podejście do testu
Chris F Carroll

6

String.Format używa StringBuilderwewnętrznie ... tak logicznie, że prowadzi to do pomysłu, że byłby nieco mniej wydajny ze względu na większy narzut. Jednak prosta konkatenacja ciągów jest najszybszą metodą wstawienia jednego ciągu między dwa inne ... w znacznym stopniu. Ten dowód został przedstawiony przez Rico Mariani w jego pierwszym quizie wydajnościowym lata temu. Prosty fakt jest taki, że konkatenacje ... gdy znana jest liczba części struny (bez ograniczeń ... możesz połączyć tysiąc części ... o ile wiesz, że zawsze jest 1000 części) ... są zawsze szybsze niż StringBuilderlub String. Format. Mogą być wykonywane z pojedynczą alokacją pamięci i serią kopii pamięci. Oto dowód

A oto rzeczywisty kod niektórych metod String.Concat, które ostatecznie wywołują FillStringChecked, który używa wskaźników do kopiowania pamięci (wyodrębnianej przez Reflector):

public static string Concat(params string[] values)
{
    int totalLength = 0;

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }

    string[] strArray = new string[values.Length];

    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;

        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }

    return ConcatArray(strArray, totalLength);
}

public static string Concat(string str0, string str1, string str2, string str3)
{
    if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
    {
        return Empty;
    }

    if (str0 == null)
    {
        str0 = Empty;
    }

    if (str1 == null)
    {
        str1 = Empty;
    }

    if (str2 == null)
    {
        str2 = Empty;
    }

    if (str3 == null)
    {
        str3 = Empty;
    }

    int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
    string dest = FastAllocateString(length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, str0.Length, str1);
    FillStringChecked(dest, str0.Length + str1.Length, str2);
    FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
    return dest;
}

private static string ConcatArray(string[] values, int totalLength)
{
    string dest = FastAllocateString(totalLength);
    int destPos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        FillStringChecked(dest, destPos, values[i]);
        destPos += values[i].Length;
    }

    return dest;
}

private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
    int length = src.Length;

    if (length > (dest.Length - destPos))
    {
        throw new IndexOutOfRangeException();
    }

    fixed (char* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

A więc wtedy:

string what = "cat";
string inthehat = "The " + what + " in the hat!";

Cieszyć się!


w Net4 string.Format buforuje i ponownie wykorzystuje instancję StringBuilder, więc w niektórych przypadkach może być szybsza.
Chris F Carroll,

3

Aha, najszybszy byłby:

string cat = "cat";
string s = "The " + cat + " in the hat";

nie, konkatenacja ciągów jest bardzo powolna, ponieważ .NET tworzy dodatkowe kopie zmiennych łańcuchowych między operacjami konkatenacji, w tym przypadku: dwie dodatkowe kopie plus ostatnia kopia do przypisania. Wynik: wyjątkowo słaba wydajność, w porównaniu z StringBuilderktórą w pierwszej kolejności zoptymalizowano ten typ kodowania.
Abel

Najszybciej chyba;)
UpTheCreek

2
@Abel: W odpowiedzi może brakować szczegółów, ale to podejście JEST najszybszą opcją w tym konkretnym przykładzie. Kompilator przekształci to w pojedyncze wywołanie String.Concat (), więc zamiana na StringBuilder faktycznie spowolni kod.
Dan C.

1
@Vaibhav ma rację: w tym przypadku konkatenacja jest najszybsza. Oczywiście różnica byłaby nieznaczna, chyba że powtórzyłaby się bardzo wiele razy lub być może operowała na znacznie, znacznie większej strunie.
Ben Collins,

0

To naprawdę zależy. W przypadku małych ciągów z kilkoma konkatenacjami w rzeczywistości szybciej jest po prostu dołączyć ciągi.

String s = "String A" + "String B";

Ale w przypadku większych ciągów (bardzo bardzo dużych ciągów) bardziej wydajne jest użycie StringBuilder.


0

W obu powyższych przypadkach chcę wstrzyknąć jeden lub więcej ciągów w środek wstępnie zdefiniowanego ciągu szablonu.

W takim przypadku sugerowałbym, aby String.Format był najszybszy, ponieważ został zaprojektowany w tym właśnie celu.



-1

Sugerowałbym, że nie, ponieważ String.Format nie był przeznaczony do konkatenacji, był przeznaczony do formatowania danych wyjściowych różnych danych wejściowych, takich jak data.

String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.