Optimierung der Zeichenkettenoperation in C #

7

Der folgende C # -Code benötigt 5 Minuten zum Ausführen:

%Vor%

"Die Optimierung" bewirkt, dass es in 1,5 Sekunden läuft:

%Vor%

BEARBEITEN: Einige Leute haben vorgeschlagen, StringBuilder zu verwenden, was ebenfalls ein ausgezeichneter Vorschlag ist, und dies kommt auf 0,06s hinaus:

%Vor%

Herumspielen, um den optimalen Wert von j zu finden, ist ein Thema für eine andere Zeit, aber warum genau funktioniert diese nicht offensichtliche Optimierung so gut? Außerdem habe ich zu einem verwandten Thema gehört, dass man niemals den Operator + mit Strings verwenden sollte, zugunsten von string.Format() , ist das wahr?

    
Matthew Scharley 11.11.2008, 23:12
quelle

7 Antworten

7

Sie werden wahrscheinlich sehen, dass die ersten 1000 Zeichen fast keine Zeit im Gegensatz zu den letzten 1000 Zeichen benötigen.

Ich würde annehmen, dass der zeitaufwändige Teil das tatsächliche Kopieren der großen Zeichenfolge in einen neuen Speicherbereich ist, jedes Mal wenn Sie ein Zeichen hinzufügen, das die harte Arbeit für Ihren Computer ist.

Ihre Optimierung kann leicht mit dem verglichen werden, was Sie normalerweise mit Streams machen, Sie verwenden einen Puffer. Größere Chunks führen normalerweise zu einer besseren Performance, bis Sie die kritische Größe erreicht haben, bei der es keinen Unterschied mehr macht, und beginnen, ein Nachteil zu sein, wenn Sie mit kleinen Datenmengen arbeiten.

Wenn Sie jedoch von Anfang an ein Char-Array mit der passenden Größe definiert hätten, wäre es wahrscheinlich blitzschnell, weil es dann nicht mehr und immer wieder kopiert werden muss.

    
jishi 11.11.2008, 23:21
quelle
9

Ich bekomme Ihre Ergebnisse überhaupt nicht. Auf meiner Box gewinnt StringBuilder die Hände nach unten. Könnten Sie Ihr komplettes Testprogramm posten? Hier ist meine, mit drei Varianten - Ihre String-Verkettung-Optimierung, die "einfache" StringBuilder eine, und StringBuilder mit einer anfänglichen Kapazität. Ich habe das Limit erhöht, weil es auf meiner Box zu schnell ging, um nutzbringend messbar zu sein.

%Vor%

Und die Ergebnisse:

%Vor%

Der Grund, warum Ihre Verkettung schneller ist als die allererste Lösung, ist einfach - Sie machen mehrere "billige" Verkettungen (wobei jedes Mal relativ wenig Daten kopiert werden) und relativ wenige "große" Verkettungen (der gesamten Kette) bisher). Im Original würde jeder Schritt alle bisher erhaltenen Daten kopieren, was natürlich teurer ist.

    
Jon Skeet 11.11.2008 23:35
quelle
8

Verwenden Sie StringBuilder zum Verketten von mehr als (ungefähr) fünf Zeichenfolgen (Ergebnisse) kann leicht variieren). Geben Sie außerdem dem Konstruktor des StringBuilders einen Hinweis auf die erwartete maximale Größe.

[Update]: Kommentiere einfach deine Bearbeitung zu der Frage. Sie können auch die Leistung von StringBuilder erhöhen, wenn Sie eine ungefähre (oder genaue) Vorstellung von der endgültigen Größe der verketteten Strings haben, da dies die Anzahl der Speicherzuordnungen, die ausgeführt werden müssen, reduziert:

%Vor%     
Mitch Wheat 11.11.2008 23:16
quelle
3
  

Auch zu einem verwandten Thema habe ich gehört, dass Sie sagen sollten, dass Sie den Operator + niemals mit Strings zugunsten von string.Format () verwenden sollten, ist das wahr?

Nein, wie alle absoluten Aussagen ist es Unsinn. Allerdings ist wahr, dass die Verwendung von Format den Formatierungscode normalerweise lesbarer macht und oft etwas schneller ist als die Verkettung - aber Geschwindigkeit ist hier nicht der entscheidende Faktor.

Wie für Ihren Code ... führt dies dazu, dass kleinere Zeichenketten in die Verkettung kopiert werden (nämlich tmp ). Natürlich kopiert man in fraction += tmp eine größere Zeichenkette, aber das passiert seltener.

Daher haben Sie viele große Kopien auf einige wenige große und viele kleine Kopien reduziert.

Hmm, mir ist gerade aufgefallen, dass Ihre äußere Schleife in beiden Fällen gleich groß ist. Das sollte dann nicht schneller sein.

    
Konrad Rudolph 11.11.2008 23:16
quelle
3

Ich kann jetzt keine Tests durchführen, aber versuche StringBuilder zu verwenden.

%Vor%     
Zote 11.11.2008 23:16
quelle
1

Beantworten Sie die modifizierte Frage ("Warum funktioniert diese nicht-offensichtliche Optimierung so gut" und "Ist es wahr, dass Sie keinen + Operator für Strings verwenden sollten"):

Ich bin mir nicht sicher, über welche nicht-offensichtliche Optimierung Sie sprechen. Aber die Antwort auf die zweite Frage deckt, denke ich, alle Grundlagen ab.

Die Funktionsweise von Zeichenfolgen in C # besteht darin, dass sie als feste Länge zugewiesen sind und nicht geändert werden können. Dies bedeutet, dass bei jedem Versuch, die Länge der Zeichenfolge zu ändern, eine vollständig neue Zeichenfolge erstellt und die alte Zeichenfolge bis zur richtigen Länge kopiert wird. Dies ist offensichtlich ein langsamer Prozess. Wenn Sie String.Format verwenden, verwendet es intern einen StringBuilder, um die Zeichenfolge zu erstellen.

StringBuilders verwenden einen Speicherpuffer, der intelligenter als Strings fester Länge zugewiesen wird und daher in den meisten Situationen deutlich besser funktioniert. Ich bin mir intern nicht sicher über die Details von StringBuilder, also musst du eine neue Frage stellen. Ich kann spekulieren, dass es entweder die alten Teile der Zeichenkette nicht neu zuordnet (statt dessen intern eine verknüpfte Liste zu erstellen und die endgültige Ausgabe tatsächlich zuweist, wenn es von ToString benötigt wird) oder es mit exponentiellem Wachstum neu zuordnet (wenn es nicht genügend Speicher hat) doppelt so viel beim nächsten Mal, also für eine 2GB-Zeichenfolge müsste es nur etwa 30 mal neu zuweisen).

Ihr Beispiel mit den verschachtelten Schleifen wächst linear. es nimmt eine kleine Schnur und wächst diese bis zu 1000, und heftet dann diese 1000 an die größere Schnur in einer großen Operation. Da die große Zeichenfolge sehr groß wird, wird die Kopie, die beim Erstellen einer neuen Zeichenfolge entsteht, sehr lange dauern. Wenn Sie die Anzahl der Wiederholungen verringern (indem Sie stattdessen stattdessen eine kleinere Zeichenkette skalieren), erhöhen Sie die Geschwindigkeit. Natürlich ist StringBuilder sogar noch schlauer bei der Speicherzuweisung und damit viel schneller.

    
SoapBox 11.11.2008 23:52
quelle
1

Das Hinzufügen eines Zeichens zu einer Zeichenfolge kann zwei Konsequenzen haben:

  • wenn noch Platz für das Zeichen ist, das am Ende hinzugefügt wurde; (Wie ein Kommentator bemerkt hat, kann dies nicht mit c # Strings passieren, da Sie unveränderlich sind).
  • Wenn am Ende kein Platz ist, wird ein neuer Speicherblock für die neue Zeichenfolge zugewiesen, der Inhalt der alten Zeichenfolge wird dort kopiert und das Zeichen wird hinzugefügt.

Um Ihren Code zu analysieren, ist es einfacher, 1000000 Mal ein einzelnes Zeichen hinzuzufügen. Ihr genaues Beispiel ist etwas komplizierter zu erklären, da Sie für höhere i-Werte mehrere Zeichen gleichzeitig hinzufügen können.

Dann, in dem Fall, dass kein zusätzlicher Speicherplatz reserviert ist, muss das erste Beispiel 1000000 Zuweisungen und Kopien von durchschnittlich 0,5 * 1000000 Zeichen vornehmen. Die zweite muss 1000 Zuweisungen und Kopien von durchschnittlich 0,5 * 1000000 Zeichen und 1000000 Zuweisungen und Kopien von 0,5 * 1000 Zeichen vornehmen. Handelt es sich um eine Kopie mit der Größe der Kopie und ohne Zuteilung, beträgt die erste Situation 500 000 000 000 Zeiteinheiten und die zweite 500 000 000 + 500 000 000 Zeiteinheiten.

    
Stephan Eggermont 11.11.2008 23:59
quelle