En un la pregunta anterior 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?

  • Voy a hacer algo de tiempo ;-p
  • Para los lectores de la claridad, era sobre el uso de una sola StringBuilder, vs varios de la cadena.Únete, que luego se unió a (n+1 une)
  • La diferencia en el rendimiento rápidamente se ejecuta hasta varios órdenes de magnitud. Si haces más de un puñado de combinaciones, se puede obtener un gran rendimiento al cambiar a stringbuilder
InformationsquelleAutor Hosam Aly | 2009-02-25

6 Comentarios

  1. 101

    Respuesta corta: depende.

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

    String.Join puede mirar a través de todas las cadenas de texto para trabajar la longitud exacta que necesita, y luego ir de nuevo y copiar todos los datos. Esto significa que habrá no extra copia de los involucrados. El sólo desventaja es que tiene que ir a través de las cadenas de dos veces, lo que significa que potencialmente soplado de la memoria caché más veces de lo necesario.

    Si no tienen las cuerdas como una matriz de antemano, es probablemente más rápido utilizar StringBuilder – pero habrá situaciones en las que no es. Si se utiliza un StringBuilder significa hacer muchas copias, en la construcción de una matriz y, a continuación, llamar a String.Join bien puede ser más rápido.

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

    EDIT: Cuando estoy en casa, voy a escribir hasta 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 tiene derecho, usted debe ser capaz de forzar una copia para cada append (de elementos, no de la delimitador, a pesar de que debe ser tomada en cuenta también). En ese punto es casi tan mala como la concatenación de cadena simple – pero String.Join no tendrá problemas.

    • Incluso cuando no tengo las cuerdas de antemano, parece más rápido que el uso de la Cadena.Unirse. Por favor revise mi respuesta…
    • En dependerá de cómo la matriz se produce, su tamaño, etc. Estoy feliz de darle una forma bastante definitivo «En <esto> caso de la Cadena.Únete va a ser al menos tan rápido» – no me gusta hacer a la inversa.
    • (En particular, mira Marc respuesta, donde StringBuilder beats Cadena de entrada y salida.Unirse, sólo acerca de. La vida es complicada.)
    • Creo que te has implícita esto, pero mi entendimiento es que usted puede mejorar StringBuilder rendimiento de la configuración de su capacidad lo suficientemente alto hasta el frente, de modo que pocos o ningún extra copias se realizan. Por supuesto, esto supone que su código puede conocer algo acerca de los tamaños de los involucrados.
    • Pero en el caso de una sola llamada a la Cadena.Únete vs sola llamada a StringBuilder, luego de Cadena.Únete gana, ¿de acuerdo? (Me parece que es importante tener en cuenta ya que esta página obtener éxitos gracias a su rimbombante título, sin importar el escenario en particular se menciona en el OP pregunta)
    • ¿Te refieres a la construcción de una StringBuilder con una cadena original, a continuación, llamar a Append una vez? Sí, me gustaría esperar string.Join ganar allí.
    • [Hilo nigromancia]: Corriente (.NET 4.5) implementación de string.Join utiliza StringBuilder.

  2. 30

    Aquí está mi equipo de prueba, utilizando int[][] de simplicidad; los resultados de la primera:

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

    (actualización de double resultados:)

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

    (actualización de re 2048 * 64 * 150)

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

    y con OptimizeForTesting habilitado:

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

    De modo más rápido, pero no tan masivamente; rig (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();
    }
    }
    }
    • Gracias Marc. ¿Qué se obtiene de las matrices más grandes? Estoy usando [2048][64] por ejemplo (alrededor de 1 MB). También hacen sus resultados, de todos modos se diferencian si el uso de la OptimizeForTesting() método que estoy usando?
    • Va a ejecutar y actualizar…
    • Muchas gracias Marc. Pero me doy cuenta de que esta no es la primera vez que se obtienen resultados diferentes para los micro-puntos de referencia. ¿Tiene usted alguna idea de por qué puede ser?
    • El Karma? Los rayos cósmicos? Quién sabe… a lo que muestra los peligros de micro-optimización, aunque ;-p
    • Usted está utilizando un procesador AMD, por ejemplo? ET64? Tal vez tengo muy poca memoria caché (512 KB)? O tal vez el .NET framework en Windows Vista está más optimizado que el de XP SP3? ¿Qué te parece? Estoy muy interesado en por qué está pasando esto…
    • XP SP3 x86, Intel Core2 Duo [email protected]
    • Gracias. Supongo que puede tener que ver con una mejora de procesador de la canalización, a continuación,…
    • Lo siento, pero cada vez que alguien menciona la «micro-optimización» y cuerdas, tienes que enlace a Atwood del clásico, «La Triste Tragedia de Micro-Optimización de Teatro«. Así lo hice, un par de años de retraso. ;^)

  3. 18

    Yo no lo creo. Mirando a través de Reflector, la aplicación de String.Join se ve muy optimizado. También tiene el beneficio añadido de saber el tamaño total de la cadena ser creado de antemano, por lo que no necesita ningún tipo de reasignación.

    He creado dos métodos de prueba para la comparación:

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

    Corrí cada método de 50 veces, al pasar un array de tamaño [2048][64]. Yo hice esto por dos arrays, uno lleno de ceros y otro lleno de valores aleatorios. He obtenido los siguientes resultados en mi máquina (P4 3.0 GHz de un solo núcleo, sin HT, ejecutando el modo de disparador de 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

    Aumentar el tamaño de la matriz a [2048][512], mientras que disminuye el número de iteraciones a 10 me dieron 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 que la mayoría de las veces (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));
    }
    }
  4. 10

    A menos que el 1% de diferencia, se convierte en algo significativo en términos del tiempo que todo el programa se lleva a ejecutar, esto se ve como micro-optimización. Me gustaría escribir el código más legible/comprensible y no te preocupes por el 1% de diferencia de rendimiento.

    • Yo creo que la Cadena.Unirse es más comprensible, pero el mensaje era más de un desafío divertido. 🙂 Es también útil (en mi humilde opinión) para aprender que el uso de un par de métodos integrados puede ser mejor que hacerlo a mano, incluso cuando la intuición podría sugerir lo contrario. …
    • … Normalmente, muchas personas han sugerido el uso de StringBuilder. Incluso si la Cadena.Únete a demostrado ser un 1% más lento, muchas personas no han pensado en ello, porque no creo StringBuilder es más rápido.
    • Yo no tengo ningún problema con la investigación, pero ahora que usted tiene una respuesta que yo no estoy seguro de que el rendimiento es la principal preocupación. Ya puedo pensar en ninguna razón para construir una cadena en formato CSV, excepto para escribir a un arroyo, que probablemente no volvería a construir el intermedio de la cadena en todo.
  5. -3

    sí. Si haces más de un par de combinaciones, será mucho más rápido.

    Cuando usted hace una cadena.únete a, el tiempo de ejecución tiene que:

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

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

    StringBuilder asigna un búfer con espacio de sobra, por lo que la información puede ser añadido sin tener que copiar la cadena original. Como no hay espacio en el buffer, el anexado de la cadena puede ser escrito en el buffer directamente.
    A continuación, sólo tiene que copiar toda la cadena una vez, al final.

    • Pero La Cadena.Únete sabe de antemano cuánto asignar, mientras que StringBuilder no. Por favor vea mi respuesta para más aclaración.
    • Cómo lo sabes?
    • Usted puede ver el código de Cadena.Unirse en el Reflector. red-gate.com/products/reflector/index.htm

Dejar respuesta

Please enter your comment!
Please enter your name here