Pregunta

En una pregunta anterior href="https://stackoverflow.com/questions/585215/can-i-rewrite-this-more-elegantly-using-linq"> sobre el formato de un double[][] a formato CSV, se sugirió que el uso de StringBuilder sería más rápido que String.Join. ¿Es esto cierto?

¿Fue útil?

Solución

Respuesta corta:. Depende

Respuesta larga: si ya tiene una matriz de cadenas para concatenar juntos (con un delimitador), String.Join es la manera más rápida de hacerlo

.

String.Join puede mirar a través de todas las cuerdas para calcular la longitud exacta que necesita, y luego ir de nuevo y copiar todos los datos. Esto significa que habrá no copia adicional implicado. El solamente inconveniente es que tiene que pasar por las cuerdas dos veces, lo que significa que potencialmente soplando la memoria caché más veces de lo necesario.

Si no tener las cadenas como una matriz de antemano, es probablemente más rápido de usar StringBuilder - pero habrá situaciones en las que no lo es. Si se utiliza un StringBuilder significa hacer montones y montones de copias, a continuación, la construcción de una matriz y luego llamar String.Join bien puede ser más rápido.

EDIT: Esto es en términos de una sola llamada a String.Join contra un montón de llamadas a StringBuilder.Append. En la pregunta original, que tenía dos niveles diferentes de llamadas String.Join, por lo que cada una de las llamadas anidadas habríamos creado una cadena intermedia. En otras palabras, es aún más complejo y más difícil de adivinar. Me sorprendería de ver de cualquier manera "ganar" significativamente (en términos de complejidad) con datos típicos.

EDIT: Cuando estoy en casa, voy a escribir un punto de referencia que es tan doloroso como, posiblemente, para StringBuilder. Básicamente, si usted tiene una matriz donde cada elemento es aproximadamente el doble del tamaño de la anterior, y se obtiene lo justo, usted debería ser capaz de forzar una copia para cada modo de adición (de elementos, no de delimitador, a pesar de que tiene que debe tenerse en cuenta también). En ese punto que es casi tan malo como la concatenación de cadenas simples - pero String.Join no tendrá problemas

.

Otros consejos

Aquí está mi banco de pruebas, utilizando int[][] por simplicidad; Primeros resultados:

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

(actualización para double resultados:)

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

(actualización re 2048 * 64 * 150)

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

y activar con OptimizeForTesting:

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

Por lo tanto más rápido, pero no tan masivamente; aparejo (ejecutar en la consola, en modo de lanzamiento, etc.):

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();
        }
    }
}

Yo no lo creo. Mirando a través del reflector, la implementación de String.Join se ve muy optimizado. También tiene la ventaja añadida de saber el tamaño total de la cadena que se ha creado de antemano, por lo que no necesita ninguna reasignación.

He creado dos métodos de prueba para compararlos:

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();
}

me encontré con cada método 50 veces, pasando en una matriz de tamaño [2048][64]. Hice esto por dos matrices; uno lleno de ceros y otro lleno de valores aleatorios. Me dieron los siguientes resultados en mi máquina (P4 a 3,0 GHz, de un solo núcleo, sin HT, que se ejecutan de modo de lanzamiento 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

El aumento del tamaño de la matriz a [2048][512], mientras que disminuye el número de iteraciones a 10 me consiguió los siguientes resultados:

// 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

Los resultados son repetibles (casi; con pequeñas fluctuaciones causadas por diferentes valores aleatorios). Al parecer String.Join es un poco más rápido la mayoría del tiempo (aunque por un margen muy pequeño).

Este es el código que he usado para las pruebas:

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));
    }
}

A menos que el 1% de diferencia se convierte en algo significativo en términos del tiempo de todo el programa necesita para funcionar, esto se parece a la micro-optimización. Me gustaría escribir el código que es la más legible / comprensible y no preocuparse por la diferencia de rendimiento 1%.

Atwood tenía una especie de mensaje relacionado con esto hace un mes:

http://www.codinghorror.com/blog/archives/001218.html

Sí. Si lo hace más de un par de combinaciones, será mucho más rápido.

Cuando usted hace una string.join, el tiempo de ejecución tiene que:

  1. Asignar memoria para la cadena resultante
  2. copiar el contenido de la primera cuerda al principio de la cadena de salida
  3. copiar el contenido de la segunda cadena al final de la cadena de salida.

Si lo hace dos combinaciones, tiene que copiar los datos dos veces, y así sucesivamente.

StringBuilder asigna un tampón con espacio de sobra, lo que los datos se pueden añadir sin tener que copiar la cadena original. Como no hay espacio de sobra en el búfer, la cadena anexa se puede escribir directamente en la memoria intermedia. Entonces sólo tiene que copiar toda la cadena de una vez, al final.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top