Frage

In einer vorherigen Frage über die Formatierung einen double[][] zu CSV-Format, href="https://stackoverflow.com/questions/585215/can-i-rewrite-this-more-elegantly-using-linq/585223#585223"> StringBuilder wäre schneller als String.Join. Ist das wahr?

War es hilfreich?

Lösung

Kurze Antwort:. Es hängt

Lange Antwort: , wenn Sie bereits ein Array von Strings haben zusammen zu verketten (mit Trennzeichen), String.Join ist der schnellste Weg, es zu tun

.

String.Join können alle Saiten schauen durch die genaue Länge zu arbeiten, es braucht, dann gehen Sie wieder, und kopieren Sie alle Daten. Das bedeutet, es wird nicht zusätzliche Kopieren beteiligt. Die nur Nachteil ist, dass es zweimal durch die Saiten gehen, die möglicherweise bedeutet, dass die Cache-Speicher mehr Zeit als nötig blasen.

Wenn Sie nicht haben die Saiten als Array vorher, es ist wahrscheinlich schneller StringBuilder verwenden - aber es wird Situationen geben, in denen dies nicht der Fall. Wenn ein StringBuilder mit bedeutet jede Menge Kopien tun, dann ein Array zu bauen und dann String.Join Aufruf schneller kann gut sein.

EDIT: Dies ist in Bezug auf einen einzigen Aufruf gegen ein Bündel von Anrufen an String.Join StringBuilder.Append. In der ursprünglichen Frage, wir hatten zwei verschiedene Ebenen der String.Join Anrufe, so dass jede der verschachtelten Anrufe hätte eine Zwischenkette geschaffen. Mit anderen Worten, es ist noch komplexer und schwieriger zu erraten zu. Ich wäre überrascht, so oder so „gewinnen“ signifikant (in Komplexität Bedingungen) mit typischen Daten zu sehen.

EDIT: Wenn ich zu Hause bin, werde ich eine Benchmark aufzuschreiben, die als möglicherweise für StringBuilder als schmerzhaft ist. Grundsätzlich, wenn Sie einen Array haben, wobei jedes Element ist etwa doppelt so groß wie die vorherigen, und Sie bekommen es genau richtig, sollen Sie in der Lage sein, eine Kopie zu zwingen, für jeden append (von Elementen, nicht des Trennzeichens, obwohl das muss wird auch berücksichtigt). An diesem Punkt ist es fast so schlimm wie einfache String-Verkettung - aber String.Join werden keine Probleme haben

.

Andere Tipps

Hier ist mein Prüfstand, mit int[][] der Einfachheit halber; Ergebnisse zuerst:

Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000

(update für double Ergebnisse:)

Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000

(update re 2048 * 64 * 150)

Join: 11620ms (chk: 206409600
OneBuilder: 11132ms (chk: 206409600

und mit OptimizeForTesting aktiviert:

Join: 11180ms (chk: 206409600
OneBuilder: 10784ms (chk: 206409600

So schneller, aber nicht massiv so; rig (Betrieb bei Konsole, im Release-Modus, usw.):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Collect()
        {
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
        }
        static void Main(string[] args)
        {
            const int ROWS = 500, COLS = 20, LOOPS = 2000;
            int[][] data = new int[ROWS][];
            Random rand = new Random(123456);
            for (int row = 0; row < ROWS; row++)
            {
                int[] cells = new int[COLS];
                for (int col = 0; col < COLS; col++)
                {
                    cells[col] = rand.Next();
                }
                data[row] = cells;
            }
            Collect();
            int chksum = 0;
            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += Join(data).Length;
            }
            watch.Stop();
            Console.WriteLine("Join: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Collect();
            chksum = 0;
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += OneBuilder(data).Length;
            }
            watch.Stop();
            Console.WriteLine("OneBuilder: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Console.WriteLine("done");
            Console.ReadLine();
        }
        public static string Join(int[][] array)
        {
            return String.Join(Environment.NewLine,
                    Array.ConvertAll(array,
                      row => String.Join(",",
                        Array.ConvertAll(row, x => x.ToString()))));
        }
        public static string OneBuilder(IEnumerable<int[]> source)
        {
            StringBuilder sb = new StringBuilder();
            bool firstRow = true;
            foreach (var row in source)
            {
                if (firstRow)
                {
                    firstRow = false;
                }
                else
                {
                    sb.AppendLine();
                }
                if (row.Length > 0)
                {
                    sb.Append(row[0]);
                    for (int i = 1; i < row.Length; i++)
                    {
                        sb.Append(',').Append(row[i]);
                    }
                }
            }
            return sb.ToString();
        }
    }
}

Das glaube ich nicht so. Beim Blick durch Reflektor, sieht die Umsetzung von String.Join sehr optimiert. Es hat auch den zusätzlichen Vorteil, die Gesamtgröße der Zeichenfolge zu wissen, im Voraus erstellt werden, so dass es braucht keine Neuzuweisung.

Ich habe zwei Testmethoden erstellt, sie vergleichen:

public static string TestStringJoin(double[][] array)
{
    return String.Join(Environment.NewLine,
        Array.ConvertAll(array,
            row => String.Join(",",
                       Array.ConvertAll(row, x => x.ToString()))));
}

public static string TestStringBuilder(double[][] source)
{
    // based on Marc Gravell's code

    StringBuilder sb = new StringBuilder();
    foreach (var row in source)
    {
        if (row.Length > 0)
        {
            sb.Append(row[0]);
            for (int i = 1; i < row.Length; i++)
            {
                sb.Append(',').Append(row[i]);
            }
        }
    }
    return sb.ToString();
}

I lief jede Methode 50fache, in einer Anordnung von Größe [2048][64] vorbei. Ich tat dies für zwei Arrays; eine mit Nullen aufgefüllt und zum anderen mit Zufallswerten gefüllt. Ich habe folgende Ergebnisse auf meinem Rechner (P4 3,0 GHz, Single-Core, kein HT, laufen Release-Modus von CMD):

// with zeros:
TestStringJoin    took 00:00:02.2755280
TestStringBuilder took 00:00:02.3536041

// with random values:
TestStringJoin    took 00:00:05.6412147
TestStringBuilder took 00:00:05.8394650

Die Erhöhung der Größe des Arrays [2048][512], während die Verringerung der Anzahl der Iterationen zu 10 bekam ich folgende Ergebnisse:

// with zeros:
TestStringJoin    took 00:00:03.7146628
TestStringBuilder took 00:00:03.8886978

// with random values:
TestStringJoin    took 00:00:09.4991765
TestStringBuilder took 00:00:09.3033365

Die Ergebnisse sind wiederholbar (fast, mit kleinen Schwankungen durch unterschiedliche Zufallswerte verursacht). Offenbar ist String.Join ein wenig schneller die meiste Zeit (wenn auch durch eine sehr geringe Marge).

Dies ist der Code, den ich für den Test verwendet:

const int Iterations = 50;
const int Rows = 2048;
const int Cols = 64; // 512

static void Main()
{
    OptimizeForTesting(); // set process priority to RealTime

    // test 1: zeros
    double[][] array = new double[Rows][];
    for (int i = 0; i < array.Length; ++i)
        array[i] = new double[Cols];

    CompareMethods(array);

    // test 2: random values
    Random random = new Random();
    double[] template = new double[Cols];
    for (int i = 0; i < template.Length; ++i)
        template[i] = random.NextDouble();

    for (int i = 0; i < array.Length; ++i)
        array[i] = template;

    CompareMethods(array);
}

static void CompareMethods(double[][] array)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < Iterations; ++i)
        TestStringJoin(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringJoin    took " + stopwatch.Elapsed);

    stopwatch.Reset(); stopwatch.Start();
    for (int i = 0; i < Iterations; ++i)
        TestStringBuilder(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringBuilder took " + stopwatch.Elapsed);

}

static void OptimizeForTesting()
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process currentProcess = Process.GetCurrentProcess();
    currentProcess.PriorityClass = ProcessPriorityClass.RealTime;
    if (Environment.ProcessorCount > 1) {
        // use last core only
        currentProcess.ProcessorAffinity
            = new IntPtr(1 << (Environment.ProcessorCount - 1));
    }
}

Es sei denn, die 1% Unterschied verwandelt sich in etwas wesentlich in Bezug auf die Zeit zu laufen das gesamte Programm dauert, das sieht aus wie Mikro-Optimierung. Ich würde den Code schreiben, die am meisten lesbar / verständlich und sich keine Sorgen über die 1% Unterschied in der Leistung ist.

ja. Wenn Sie verbindet mehr als ein paar zu tun, wird es viel schneller.

Wenn Sie eine string.join tun, die Laufzeit muss:

  1. Weisen Speicher für die resultierende Zeichenfolge
  2. kopieren Sie den Inhalt des ersten Zeichenfolge am Anfang der Ausgabezeichenfolge
  3. kopieren Sie den Inhalt des zweiten Zeichenfolge an das Ende der Ausgabezeichenfolge.

Wenn Sie das tun zwei verbindet, hat es die Daten zweimal zu kopieren, und so weiter.

String weist einen Puffer mit Raum zu ersparen, so dass die Daten können ohne angehängt werden, um die ursprüngliche Zeichenfolge zu kopieren. Da es Raum übrig bleiben im Puffer ist, kann der angehängten Zeichenfolge in den Puffer direkt geschrieben werden. Dann hat sie nur einmal die gesamte Zeichenfolge zu kopieren, am Ende.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top