Najbardziej efektywny sposób łączenia łańcuchów?


286

Jaki jest najskuteczniejszy sposób łączenia łańcuchów?


9
Chcę tu umieścić wyraźne ostrzeżenie, że zaakceptowana odpowiedź jest znacznie niepełna, ponieważ nie omawia wszystkich istotnych przypadków.
usr

@usr Rzeczywiście ... bardziej szczegółowe informacje na temat StringBuilderprzypadków użycia można znaleźć tutaj .
Tamir Vered

Moim nowym faworytem od C # 6 jest $ "Stały tekst tutaj {foo} i {bar}" ... to jest jak String.Formatna sterydach. Co pod względem wydajności jest nieco wolniejsze na jednej linii niż +i String.Concat, ale znacznie lepsze niż te, choć wolniejsze niż StringBuilderprzy wielu połączeniach. Praktycznie rzecz biorąc, różnice w wydajności są takie, że gdybym musiał wybrać tylko jeden sposób konkatenacji, wybrałbym interpolacje ciągów przy użyciu $... Jeśli dwa sposoby, dodaj StringBuilderdo mojego zestawu narzędzi. Z tymi dwoma sposobami jesteś ustawiony.
u8it

Poniższa String.Joinodpowiedź nie oddaje +sprawiedliwości i jest praktycznie złym sposobem łączenia łańcuchów, ale jest zaskakująco szybka pod względem wydajności. Odpowiedź dlaczego jest interesująca. String.Concati String.Joinoba mogą działać na tablicach, ale w String.Joinrzeczywistości jest szybszy. Najwyraźniej String.Joinjest dość wyrafinowany i bardziej zoptymalizowany niż String.Concat, częściowo dlatego, że działa podobnie do StringBuildertego, że najpierw oblicza długość łańcucha, a następnie konstruuje ciąg korzystający z tej wiedzy za pomocą UnSafeCharBuffer.
u8it

Ok, więc jest szybki, ale String.Joinwymaga także zbudowania tablicy, która wydaje się nieefektywna pod względem zasobów, prawda? ... Okazuje się +i String.Concatkonstruuje tablice dla swoich składników. W związku z tym ręczne tworzenie tablicy i karmienie jej String.Joinjest stosunkowo szybsze ... jednak StringBuilderwciąż przewyższa String.Joinpraktycznie każdy praktyczny sposób, podczas gdy $jest tylko nieco wolniejsze i znacznie szybsze przy długich ciągach… nie wspominając o tym, że korzystanie z niej jest niewygodne i brzydkie, String.Joinjeśli masz stworzyć na miejscu tablicę.
u8it

Odpowiedzi:


155

Ta StringBuilder.Append()metoda jest znacznie lepsza niż korzystanie z +operatora. Ale odkryłem, że wykonanie 1000 konkatenacji lub mniej String.Join()jest jeszcze bardziej wydajne niż StringBuilder.

StringBuilder sb = new StringBuilder();
sb.Append(someString);

Jedynym problemem String.Joinjest to, że musisz połączyć łańcuchy ze wspólnym separatorem.

Edycja: jak wskazał @ryanversaw , możesz ustawić separator string.Empty.

string key = String.Join("_", new String[] 
{ "Customers_Contacts", customerID, database, SessionID });

11
StringBuilderma ogromny porównywalny koszt rozruchu, jest skuteczny tylko przy użyciu bardzo dużych ciągów lub bardzo wielu konkatenacji. Nie jest trywialne ustalenie jakiejkolwiek sytuacji. Jeśli wydajność ma znaczenie, profilowanie jest Twoim przyjacielem (sprawdź ANTS).
Abel

32
Nie dotyczy to konkatenacji jednowierszowej. Powiedzmy, że robisz myString = „foo” + var1 + „bar” + var2 + „hello” + var3 + „world”, kompilator automatycznie zamienia to w wywołanie string.concat, które jest tak wydajne, jak to tylko możliwe. Ta odpowiedź jest niepoprawna, istnieje wiele lepszych odpowiedzi do wyboru
csauve,

2
Do trywialnego łączenia ciągów używaj tego, co jest najbardziej czytelne. ciąg a = b + c + d; prawie zawsze będzie szybsze niż robienie z StringBuilder, ale różnica zwykle nie ma znaczenia. Użyj StringBuilder (lub innej wybranej opcji), gdy wielokrotnie dodajesz do tego samego łańcucha (np. Budowanie raportu) lub gdy masz do czynienia z dużymi łańcuchami.
Swanny

5
Dlaczego nie wspomniałeś string.Concat?
Venemo

272

Rico Mariani , guru ds. Wydajności .NET, napisał artykuł na ten temat. To nie jest tak proste, jak można się spodziewać. Podstawowa rada jest następująca:

Jeśli Twój wzór wygląda następująco:

x = f1(...) + f2(...) + f3(...) + f4(...)

to jeden konkat i jest spakowany, StringBuilder prawdopodobnie nie pomoże.

Jeśli Twój wzór wygląda następująco:

if (...) x += f1(...)
if (...) x += f2(...)
if (...) x += f3(...)
if (...) x += f4(...)

wtedy prawdopodobnie chcesz StringBuilder.

Kolejny artykuł na poparcie tego twierdzenia pochodzi od Erica Lipperta, w którym szczegółowo opisuje optymalizacje przeprowadzone na +konkatenacjach jednej linii .


1
Co z String.Format ()?
IronSlug

86

Istnieje 6 rodzajów konkatenacji łańcuchów:

  1. Korzystanie z +symbolu plus ( ).
  2. Korzystanie string.Concat().
  3. Korzystanie string.Join().
  4. Korzystanie string.Format().
  5. Korzystanie string.Append().
  6. Korzystanie StringBuilder.

W eksperymencie udowodniono, że string.Concat()jest to najlepszy sposób podejścia, jeśli słowa są mniejsze niż 1000 (w przybliżeniu) i jeśli słowa są większe niż 1000, StringBuildernależy ich użyć.

Aby uzyskać więcej informacji, sprawdź tę stronę .

string.Join () vs string.Concat ()

Metoda string.Concat tutaj jest równoważna wywołaniu metody string.Join z pustym separatorem. Dołączanie pustego ciągu jest szybkie, ale nie robi tego nawet szybciej, więc metoda string.Concat byłaby tutaj lepsza.


4
Powinien przeczytać, że udowodniono, że string.Concat () lub + jest najlepszym sposobem. Tak, mogę pobrać to z artykułu, ale oszczędza mi to jedno kliknięcie. Tak więc + i concat kompilują się w ten sam kod.
brumScouse,

Użyłem tej podstawy, aby spróbować uczynić moją metodę bardziej wydajną, w której musiałem tylko połączyć dokładnie 3 łańcuchy. Okazało się, że +był faktycznie 3 milisekund szybciej niż string.Concat(), choć nie wyglądał na wysokości strun wymaganej przed string.Concat()outraces +.
Gnemlock,

59

Od Chinh Do - StringBuilder nie zawsze jest szybszy :

Reguły kciuka

  • Podczas konkatenacji trzech wartości dynamicznych ciągów lub mniej, użyj tradycyjnej konkatenacji ciągów.

  • Podczas łączenia więcej niż trzech dynamicznych wartości ciągu użyj StringBuilder.

  • Budując duży ciąg z kilku literałów łańcuchowych, użyj @literału łańcuchowego lub operatora inline +.

Większość czasu StringBuilderjest najlepszym wyborem, ale są przypadki pokazane w tym poście, że powinieneś przynajmniej pomyśleć o każdej sytuacji.


8
afaik @ wyłącza tylko przetwarzanie sekwencji specjalnych. msdn.microsoft.com/en-us/library/362314fe.aspx zgadzam się
abatishchev

12

Jeśli pracujesz w pętli, StringBuilderprawdopodobnie jest to właściwa droga; oszczędza ci to nakładów związanych z regularnym tworzeniem nowych ciągów. W kodzie, który uruchomi się tylko raz, String.Concatprawdopodobnie jest w porządku.

Jednak Rico Mariani (guru optymalizacji .NET) przygotował quiz, w którym stwierdził na końcu, że w większości przypadków zaleca String.Format.


Od lat polecam użycie string.format zamiast string + string osobom, z którymi pracowałem. Myślę, że korzyści z czytelności są dodatkową przewagą poza korzyściami związanymi z wydajnością.
Scott Lawrence

1
To jest prawdziwa poprawna odpowiedź. Aktualnie akceptowana odpowiedź na StringBuilder jest niepoprawna, ponieważ nie wspomina o dołączaniu pojedynczych wierszy, dla których string.concat lub + jest szybszy. Mało znanym faktem jest to, że kompilator faktycznie tłumaczy + na string.concat. Również dla pętli lub wielu concats linii Używam niestandardowy zbudowany ciąg budowniczy że gdy tylko dopisuje .ToString nazywa - przezwyciężenie problemu nieokreślony bufora że StringBuilder ma
csauve

2
string.Format nie jest najszybszym sposobem w żadnych okolicznościach. Nie wiem, jak wymyślić przypadek, w którym ma miejsce.
usr

@usr - zauważ, że Rico wyraźnie nie twierdzi, że jest najszybszy , tylko że jest to jego zalecenie: „Mimo że jest najgorzej działający i wiemy o tym z dużym wyprzedzeniem, obaj architekci CLR Performance Architekci zgadzają się, że [format.format] powinien być domyślnym wyborem. W bardzo mało prawdopodobnym przypadku, gdy staje się on problemem, problem można łatwo rozwiązać za pomocą tylko niewielkich lokalnych zmian. Zwykle zarabiasz na ładnej konserwacji. ”
Adam V

@AdamV pytanie dotyczy najszybszego sposobu. Nie zgadzam się z tym, że jest to domyślny wybór, choć nie z powodów doskonałych. Może to być niezdarna składnia. Resharper może konwertować tam iz powrotem do woli.
usr

10

Oto najszybsza metoda, którą opracowałem przez dekadę dla mojej aplikacji NLP na dużą skalę. Mam wariacje IEnumerable<T>i inne typy danych wejściowych, z separatorami różnych typów i bez nich ( Char, String), ale tutaj pokazuję prosty przypadek połączenia wszystkich łańcuchów w tablicy w pojedynczy łańcuch bez separatora. Najnowsza wersja tutaj została opracowana i przetestowana na C # 7 i .NET 4.7 .

Istnieją dwa klucze do wyższej wydajności; pierwszy polega na wstępnym obliczeniu wymaganego całkowitego rozmiaru. Ten krok jest trywialny, gdy dane wejściowe są tablicą, jak pokazano tutaj. Do obsługi IEnumerable<T>zamiast tego warto najpierw zebrać ciągi do tymczasowej tablicy do obliczenia tej sumy (tablica jest wymagana, aby uniknąć wywoływania ToString()więcej niż raz na element, ponieważ technicznie, biorąc pod uwagę możliwość wystąpienia efektów ubocznych, może to zmienić oczekiwaną semantykę operacji „łączenia ciągu”).

Następnie, biorąc pod uwagę całkowitą wielkość alokacji końcowego łańcucha, największy wzrost wydajności uzyskuje się poprzez zbudowanie łańcucha wynikowego w miejscu . Wykonanie tego wymaga (być może kontrowersyjnej) techniki tymczasowego zawieszenia niezmienności nowego, Stringktóry początkowo ma przypisane zera. Pomijając wszelkie takie kontrowersje ...

... zauważ, że jest to jedyne rozwiązanie konkatenacji zbiorczej na tej stronie, które całkowicie eliminuje dodatkową rundę alokacji i kopiowania przez Stringkonstruktora.

Pełny kod:

/// <summary>
/// Concatenate the strings in 'rg', none of which may be null, into a single String.
/// </summary>
public static unsafe String StringJoin(this String[] rg)
{
    int i;
    if (rg == null || (i = rg.Length) == 0)
        return String.Empty;

    if (i == 1)
        return rg[0];

    String s, t;
    int cch = 0;
    do
        cch += rg[--i].Length;
    while (i > 0);
    if (cch == 0)
        return String.Empty;

    i = rg.Length;
    fixed (Char* _p = (s = new String(default(Char), cch)))
    {
        Char* pDst = _p + cch;
        do
            if ((t = rg[--i]).Length > 0)
                fixed (Char* pSrc = t)
                    memcpy(pDst -= t.Length, pSrc, (UIntPtr)(t.Length << 1));
        while (pDst > _p);
    }
    return s;
}

[DllImport("MSVCR120_CLR0400", CallingConvention = CallingConvention.Cdecl)]
static extern unsafe void* memcpy(void* dest, void* src, UIntPtr cb);

Powinienem wspomnieć, że ten kod ma niewielką modyfikację w stosunku do tego, z czego sam korzystam. W oryginale, ja wywołać cpblk IL dyspozycję z C # zrobić rzeczywiste kopiowanie. Dla uproszczenia i przenośności w kodzie tutaj zastąpiłem to P / Invoke memcpy, jak widać. Aby uzyskać najwyższą wydajność na x64 ( ale może nie x86 ), możesz zamiast tego użyć metody cpblk .


string.Joinrobi te wszystkie rzeczy już dla ciebie. Nie musisz pisać tego sam. Oblicza rozmiar końcowego łańcucha, konstruuje łańcuch o tym rozmiarze, a następnie zapisuje do podstawowej tablicy znaków. Ma nawet zaletę używania czytelnych nazw zmiennych w tym procesie.
Servy,

1
@Servy Dzięki za komentarz; rzeczywiście String.Joinmoże być wydajny. Jak wspomniałem we wstępie, kod tutaj jest tylko najprostszą ilustracją rodziny funkcji, których używam do scenariuszy, które String.Joinalbo nie obsługują (takie jak optymalizacja dla Charseparatora), albo nie obsługiwały poprzednich wersji .NET. Przypuszczam, że nie powinienem był wybierać tego w najprostszym przykładzie, ponieważ jest to przypadek, który String.Joinjuż dobrze sobie radzi, choć z „nieefektywnością”, prawdopodobnie niemożliwą do zmierzenia, w przetwarzaniu próżnego separatora, a mianowicie. String.Empty.
Glenn Slayden,

Oczywiście, jeśli nie masz separatora, powinieneś zadzwonić Concat, co również robi to poprawnie. Tak czy inaczej, nie musisz sam pisać kodu.
Servy,

7
@Servy Porównałem wydajność String.Joinmojego kodu za pomocą tego zestawu testowego . W przypadku 10 milionów losowych operacji konkatenacji, z których każda zawiera do 100 ciągów o wielkości słowa, powyższy kod jest konsekwentnie o 34% szybszy niż String.Joinw wersji x64 z .NET 4.7 . Ponieważ PO wyraźnie żąda metody „najbardziej wydajnej”, wynik sugeruje, że moja odpowiedź ma zastosowanie. Jeśli rozwiąże to twoje obawy, zapraszam do ponownego rozpatrzenia swojego zdania.
Glenn Slayden,

1
Niedawno dokonałem testu porównawczego na pełnej wersji CLR 4.7.1 x64 i stwierdziłem, że jest on około dwa razy szybszy niż string. Dołącz do niego z przydzieloną około 25% mniejszą pamięcią ( i.imgur.com/SxIpEmL.png ) podczas korzystania z cpblk lub github.com/ JonHanna / Mnemosyne
quentin-starin

6

Z tego artykułu MSDN :

Z tworzeniem obiektu StringBuilder wiąże się pewne obciążenie, zarówno czasowe, jak i pamięciowe. Na komputerze z szybką pamięcią StringBuilder staje się opłacalny, jeśli wykonujesz około pięciu operacji. Zasadniczo powiedziałbym, że 10 lub więcej operacji na łańcuchach jest uzasadnieniem narzutu na dowolnej maszynie, nawet wolniejszej.

Jeśli więc ufasz MSDN, skorzystaj z StringBuilder, jeśli musisz wykonać więcej niż 10 operacji / konkatenacji łańcuchów - w przeciwnym razie proste połączenie łańcuchów z '+' jest w porządku.



5

Dodając do innych odpowiedzi, należy pamiętać, że StringBuilder może otrzymać początkową ilość pamięci do przydzielenia .

Pojemność parametr określa maksymalną liczbę znaków, które mogą być przechowywane w pamięci przydzielonej do bieżącej instancji. Jego wartość jest przypisana do właściwości Pojemność . Jeśli liczba znaków, które mają być przechowywane w bieżącej instancji, przekracza tę wartość pojemności , obiekt StringBuilder przydziela dodatkową pamięć do ich przechowywania.

Jeśli pojemność wynosi zero, używana jest domyślna pojemność specyficzna dla implementacji.

Wielokrotne dołączanie do StringBuilder, który nie został wstępnie przydzielony, może powodować wiele niepotrzebnych przydziałów, podobnie jak powtarzające się regularne łączenie ciągów.

Jeśli wiesz, jak długi będzie końcowy ciąg, możesz go trywialnie obliczyć lub zgadnij, co to jest typowy przypadek (przydzielanie zbyt dużej liczby niekoniecznie jest złą rzeczą), powinieneś przekazać te informacje konstruktorowi lub Właściwość pojemności . Zwłaszcza podczas uruchamiania testów wydajności w celu porównania StringBuilder z innymi metodami, takimi jak String.Concat, które robią to samo wewnętrznie. Każdy test widziany online, który nie uwzględnia wstępnej alokacji StringBuilder w swoich porównaniach, jest nieprawidłowy.

Jeśli nie możesz zgadnąć, jaki jest rozmiar, prawdopodobnie piszesz funkcję narzędziową, która powinna mieć własny opcjonalny argument do kontroli wstępnej alokacji.


4

Poniżej może być jeszcze jedno alternatywne rozwiązanie do łączenia wielu ciągów.

String str1 = "sometext";
string str2 = "some other text";

string afterConcate = $"{str1}{str2}";

interpolacja ciągów


1
Jest to właściwie zaskakujące, jako ogólna metoda konkatenacji. Jest w zasadzie, String.Formatale bardziej czytelny i łatwiejszy w obsłudze. Ława oznakowanie go, to nieco wolniej niż +i String.Concatna jednej linii powiązań, ale znacznie lepsze niż obie te w powtarzalnych połączeń wchodzących StringBuildermniej konieczne.
u8it

2

Najbardziej wydajne jest użycie StringBuilder, takie jak:

StringBuilder sb = new StringBuilder();
sb.Append("string1");
sb.Append("string2");
...etc...
String strResult = sb.ToString();

@jonezy: String.Concat jest w porządku, jeśli masz kilka małych rzeczy. Ale jeśli łączysz megabajty danych, Twój program prawdopodobnie się zapełni.


hej, jakie jest rozwiązanie dla megabajtów danych?
Neel

2

Wypróbuj 2 fragmenty kodu, a znajdziesz rozwiązanie.

 static void Main(string[] args)
    {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10000000; i++)
        {
            s.Append( i.ToString());
        }
        Console.Write("End");
        Console.Read();
    }

Vs

static void Main(string[] args)
    {
        string s = "";
        for (int i = 0; i < 10000000; i++)
        {
            s += i.ToString();
        }
        Console.Write("End");
        Console.Read();
    }

Przekonasz się, że 1. kod skończy się naprawdę szybko, a pamięć będzie w sporej ilości.

Drugi kod może pamięć będzie w porządku, ale zajmie to dłużej ... znacznie dłużej. Więc jeśli masz aplikację dla wielu użytkowników i potrzebujesz prędkości, użyj 1.. Jeśli masz aplikację na krótki czas dla jednej aplikacji użytkownika, być może możesz użyć obu aplikacji lub druga będzie bardziej „naturalna” dla programistów.

Twoje zdrowie.


1

W przypadku tylko dwóch ciągów zdecydowanie nie chcesz używać StringBuilder. Istnieje pewien próg, powyżej którego obciążenie StringBuilder jest mniejsze niż obciążenie przydzielania wielu ciągów.

Tak więc, dla więcej niż 2-3 łańcuchów, użyj kodu DannySmurf . W przeciwnym razie wystarczy użyć operatora +.


1

System.String jest niezmienny. Kiedy modyfikujemy wartość zmiennej łańcuchowej, nowa pamięć jest przydzielana do nowej wartości i poprzedni przydział pamięci jest zwalniany. System.StringBuilder został zaprojektowany w taki sposób, aby miał pojęcie zmiennego ciągu, w którym można wykonywać różne operacje bez przydzielania osobnej lokalizacji pamięci dla zmodyfikowanego ciągu.


1

Inne rozwiązanie:

wewnątrz pętli użyj List zamiast łańcucha.

List<string> lst= new List<string>();

for(int i=0; i<100000; i++){
    ...........
    lst.Add(...);
}
return String.Join("", lst.ToArray());;

to jest bardzo, bardzo szybkie.



0

To zależy od kodu. StringBuilder jest ogólnie bardziej wydajny, ale jeśli łączysz tylko kilka łańcuchów i robisz to wszystko w jednym wierszu, optymalizacje kodu zapewnią to za Ciebie. Ważne jest, aby pomyśleć również o tym, jak wygląda kod: w przypadku większych zestawów StringBuilder ułatwi czytanie, w przypadku małych StringBuilder po prostu doda niepotrzebnego bałaganu.

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.