String.Replace () vs. StringBuilder.Replace ()

78

Ich habe eine Zeichenfolge, in der ich Markierungen durch Werte aus einem Wörterbuch ersetzen muss. Es muss so effizient wie möglich sein. Wenn Sie eine Schleife mit einem string.replace ausführen, wird nur Speicher verbraucht (Strings sind unveränderlich, denken Sie daran). Wäre StringBuilder.Replace () besser, da dies für die Bearbeitung von String-Manipulationen entwickelt wurde?

Ich hatte gehofft, die Kosten von RegEx zu vermeiden, aber wenn das effizienter wird, dann soll es so sein.

Hinweis: Die Komplexität des Codes ist mir egal, nur wie schnell er ausgeführt wird und wie viel Speicher er verbraucht.

Durchschnittliche Statistik: 255-1024 Zeichen lang, 15-30 Schlüssel im Wörterbuch.

Dustin Davis
quelle
Wie ist das Muster (die Längen) der Marker und Werte?
Henk Holterman
Kurz. Marker 5-15, Werte 5-25
Dustin Davis
Mögliches Duplikat von stackoverflow.com/questions/287842/…
Michael Freidgeim

Antworten:

72

Verwenden von RedGate Profiler mit dem folgenden Code

class Program
    {
        static string data = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
        static Dictionary<string, string> values;

        static void Main(string[] args)
        {
            Console.WriteLine("Data length: " + data.Length);
            values = new Dictionary<string, string>()
            {
                { "ab", "aa" },
                { "jk", "jj" },
                { "lm", "ll" },
                { "yz", "zz" },
                { "ef", "ff" },
                { "st", "uu" },
                { "op", "pp" },
                { "x", "y" }
            };

            StringReplace(data);
            StringBuilderReplace1(data);
            StringBuilderReplace2(new StringBuilder(data, data.Length * 2));

            Console.ReadKey();
        }

        private static void StringReplace(string data)
        {
            foreach(string k in values.Keys)
            {
                data = data.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace1(string data)
        {
            StringBuilder sb = new StringBuilder(data, data.Length * 2);
            foreach (string k in values.Keys)
            {
                sb.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace2(StringBuilder data)
        {
            foreach (string k in values.Keys)
            {
                data.Replace(k, values[k]);
            }
        }
    }
  • String.Replace = 5.843ms
  • StringBuilder.Replace # 1 = 4.059ms
  • Stringbuilder.Replace # 2 = 0,461 ms

Stringlänge = 1456

Stringbuilder Nr. 1 erstellt den Stringbuilder in der Methode, während Nr. 2 dies nicht tut, sodass der Leistungsunterschied höchstwahrscheinlich gleich bleibt, da Sie nur diese Arbeit aus der Methode entfernen. Wenn Sie mit einem Stringbuilder anstelle eines Strings beginnen, ist möglicherweise # 2 der richtige Weg.

In Bezug auf den Arbeitsspeicher müssen Sie sich mit dem RedGateMemory-Profiler keine Sorgen machen, bis Sie zu VIELEN Ersetzungsvorgängen gelangen, bei denen der Stringbuilder insgesamt gewinnen wird.

Dustin Davis
quelle
Wenn der Compiler dies optimieren könnte, würde er die Zeile von StringBuilderReplace1 (Daten) ändern; zu StringBuilderReplace2 (neuer StringBuilder (data, data.Length * 2));? Nur neugierig. Ich verstehe den Unterschied, war nur neugierig, wenn Sie es wussten.
pqsk
1
Ich verstehe nicht, warum SB-Methode 2 so viel schneller ist - die JIT sollte sowohl SB # 1 als auch SB # 2 optimieren, damit sie zur Laufzeit gleich sind.
Dai
@Dai denken Sie daran, dies war im Jahr 2011. Die Dinge haben sich seitdem möglicherweise geändert.
Dustin Davis
7
@Dai - (späte Antwort) - Wie in der Antwort angegeben, misst der Profiler nur die verstrichene Zeit der tatsächlichen Funktion. Da die Stringbuilder-Deklaration in Replace # 2 außerhalb der Funktion liegt, ist die Konstruktionszeit nicht in der verstrichenen Zeit enthalten.
Stirrblig
Sie sagen also, dass 89% der Ersetzungszeit für StringBuilderReplace1 nur die StringBuilder-Instanz initialisiert? Und nur etwa 11% (0,461 von 4,059) der Zeit werden für das Ersetzen von Schlüsseln aufgewendet, wie in StringBuilderReplace2 isoliert? Wenn ja ... würde ich einen StringBuilder-Puffer zuweisen und ein gewisses Maß an Parallelität für die Stapelverarbeitung unter Verwendung vorhandener Instanzen einschränken. Die Frage ist nun ... was trägt StringBuilder.ToString zur Zeit bei? Um fair zu sein, ist Ihre Zielausgabe schließlich eine Zeichenfolge, und nur die erste Methode erzeugt tatsächlich eine Zeichenfolge.
Triynko
9

Dies kann hilfreich sein:

http://blogs.msdn.com/b/debuggingtoolbox/archive/2008/04/02/comparing-regex-replace-string-replace-and-stringbuilder-replace-which-has-better-performance.aspx

Die kurze Antwort scheint zu sein, dass String.Replace schneller ist, obwohl dies einen größeren Einfluss auf Ihren Speicherbedarf / Speicherbereinigungsaufwand haben kann.

RMD
quelle
interessant. Basierend auf ihrem Test war string.replace besser. Ich dachte, dass aufgrund der geringen Größe der Zeichenfolge string.replace besser wäre, wenn man den Aufwand für die Erstellung eines String-Builders in Betracht ziehen würde
Dustin Davis
6

Ja, StringBuilderSie erhalten sowohl Geschwindigkeits- als auch Speichergewinn (im Grunde genommen, weil nicht jedes Mal, wenn Sie eine Manipulation damit durchführen, eine Instanz einer Zeichenfolge erstellt wird - arbeitet StringBuilderimmer mit demselben Objekt). Hier ist ein MSDN-Link mit einigen Details.

Andrei
quelle
Aber lohnt es sich, den String Builder zu erstellen?
Dustin Davis
1
@Dustin: mit 15-30 Ersatz ist es wahrscheinlich.
Henk Holterman
@Henk - es gibt 15-30 Suchanfragen, nicht unbedingt so viele Ersetzungen. Die erwartete durchschnittliche Anzahl von Markern pro Zeichenfolge ist signifikant.
Joe
@ Joe: Es wird 15-30 Scans geben (außer vielleicht dem Regex).
Henk Holterman
@Henk, ja, aber die Leistung der 15-30 Scans ist für String und StringBuilder ähnlich. Jeder Leistungsunterschied, der den Aufwand für die Erstellung des String Builders rechtfertigt, muss durch Ersetzungen und nicht durch Scans verursacht werden.
Joe
5

Wäre stringbuilder.replace besser [als String.Replace]

Ja, viel besser. Und wenn Sie eine Obergrenze für die neue Zeichenfolge schätzen können (es sieht so aus, als könnten Sie das), ist sie wahrscheinlich schnell genug.

Wenn Sie es erstellen wie:

  var sb = new StringBuilder(inputString, pessimisticEstimate);

dann muss der StringBuilder seinen Puffer nicht neu zuordnen.

Henk Holterman
quelle
1

Das Konvertieren von Daten von einem String in einen StringBuilder und zurück dauert einige Zeit. Wenn nur eine einzige Ersetzungsoperation ausgeführt wird, wird diese Zeit möglicherweise nicht durch die Effizienzverbesserungen von StringBuilder wieder wettgemacht. Wenn man dagegen einen String in einen StringBuilder konvertiert, dann viele Ersetzungsoperationen daran ausführt und ihn am Ende wieder konvertiert, ist der StringBuilder-Ansatz wahrscheinlich schneller.

Superkatze
quelle
1

Anstatt 15 bis 30 Ersetzungsoperationen für die gesamte Zeichenfolge auszuführen, ist es möglicherweise effizienter, so etwas wie eine Trie- Datenstruktur zu verwenden, um Ihr Wörterbuch zu speichern. Anschließend können Sie Ihre Eingabezeichenfolge einmal durchlaufen, um alle Suchvorgänge / Ersetzungen durchzuführen.

Matt Bridges
quelle
1

Es hängt stark davon ab, wie viele der Marker durchschnittlich in einer bestimmten Zeichenfolge vorhanden sind.

Die Leistung bei der Suche nach einem Schlüssel ist zwischen StringBuilder und String wahrscheinlich ähnlich, aber StringBuilder gewinnt, wenn Sie viele Markierungen in einer einzelnen Zeichenfolge ersetzen müssen.

Wenn Sie durchschnittlich nur ein oder zwei Marker pro Zeichenfolge erwarten und Ihr Wörterbuch klein ist, würde ich mich einfach für String.Replace entscheiden.

Wenn es viele Markierungen gibt, möchten Sie möglicherweise eine benutzerdefinierte Syntax definieren, um Markierungen zu identifizieren - z. B. in geschweiften Klammern mit einer geeigneten Escape-Regel für eine wörtliche Klammer. Sie können dann einen Parsing-Algorithmus implementieren, der die Zeichen der Zeichenfolge einmal durchläuft und jeden gefundenen Marker erkennt und ersetzt. Oder verwenden Sie einen regulären Ausdruck.

Joe
quelle
+1 für Regex - Beachten Sie, dass der tatsächliche Ersatz in diesem Fall a verwenden kann MatchEvaluator, um die Wörterbuchsuche tatsächlich durchzuführen.
Random832
1

Meine zwei Cent hier, ich habe nur ein paar Codezeilen geschrieben, um zu testen, wie jede Methode funktioniert, und wie erwartet ist das Ergebnis "es kommt darauf an".

Bei längeren Saiten Regexscheint die Leistung besser zu sein, bei kürzeren Saiten ist String.Replacedies der Fall . Ich kann sehen, dass die Verwendung von StringBuilder.Replacenicht sehr nützlich ist, und wenn sie falsch verwendet wird, kann sie in der GC-Perspektive tödlich sein (ich habe versucht, eine Instanz von zu teilen StringBuilder).

Überprüfen Sie mein StringReplaceTests GitHub-Repo .

Zdeněk
quelle
1

Das Problem mit der Antwort von @DustinDavis ist, dass es rekursiv mit derselben Zeichenfolge arbeitet. Sofern Sie nicht vorhaben, eine Hin- und Her-Manipulation durchzuführen, sollten Sie für diese Art von Test wirklich separate Objekte für jeden Manipulationsfall haben.

Ich habe mich entschlossen, meinen eigenen Test zu erstellen, weil ich im gesamten Web einige widersprüchliche Antworten gefunden habe, und ich wollte ganz sicher sein. Das Programm, an dem ich arbeite, behandelt viel Text (in einigen Fällen Dateien mit Zehntausenden von Zeilen).

Hier ist eine schnelle Methode, die Sie kopieren und einfügen können, um selbst zu sehen, welche schneller ist. Möglicherweise müssen Sie zum Testen eine eigene Textdatei erstellen. Sie können jedoch problemlos Text von überall kopieren und einfügen und eine ausreichend große Datei für sich selbst erstellen:

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;

void StringReplace_vs_StringBuilderReplace( string file, string word1, string word2 )
{
    using( FileStream fileStream = new FileStream( file, FileMode.Open, FileAccess.Read ) )
    using( StreamReader streamReader = new StreamReader( fileStream, Encoding.UTF8 ) )
    {
        string text = streamReader.ReadToEnd(),
               @string = text;
        StringBuilder @StringBuilder = new StringBuilder( text );
        int iterations = 10000;

        Stopwatch watch1 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @string = @string.Replace( word1, word2 );
            else @string = @string.Replace( word2, word1 );
        watch1.Stop();
        double stringMilliseconds = watch1.ElapsedMilliseconds;

        Stopwatch watch2 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @StringBuilder = @StringBuilder .Replace( word1, word2 );
            else @StringBuilder = @StringBuilder .Replace( word2, word1 );
        watch2.Stop();
        double StringBuilderMilliseconds = watch1.ElapsedMilliseconds;

        MessageBox.Show( string.Format( "string.Replace: {0}\nStringBuilder.Replace: {1}",
                                        stringMilliseconds, StringBuilderMilliseconds ) );
    }
}

Ich habe diese Zeichenfolge erhalten. Ersetzen () war jedes Mal, wenn Wörter mit 8 bis 10 Buchstaben ausgetauscht wurden, um etwa 20% schneller. Probieren Sie es selbst aus, wenn Sie Ihre eigenen empirischen Beweise wollen.

Meloviz
quelle