Question

Dans un question précédente sur le formatage d'un double[][] à format CSV, il a été suggéré que l'utilisation StringBuilder serait plus rapide que String.Join. Est-ce vrai?

Était-ce utile?

La solution

Réponse courte:. Elle dépend

Réponse longue: si vous avez déjà un tableau de chaînes à concaténer ensemble (avec un séparateur), String.Join est le meilleur moyen de le faire

.

String.Join peut regarder à travers toutes les cordes pour travailler la longueur exacte dont il a besoin, puis aller à nouveau et copier toutes les données. Cela signifie qu'il y aura pas copie supplémentaire impliqué. uniquement inconvénient est qu'il doit passer par les cordes deux fois, ce qui signifie potentiellement souffler la mémoire cache plus de fois que nécessaire.

Si vous ne pas ont les cordes comme un tableau à l'avance, il est probablement plus rapide à utiliser StringBuilder - mais il y aura des situations où il n'est pas. Si vous utilisez un StringBuilder signifie faire beaucoup et beaucoup de copies, puis la construction d'un tableau, puis en appelant String.Join pourrait bien être plus rapide.

EDIT: Ceci est en termes d'un seul appel à String.Join vs un tas d'appels à StringBuilder.Append. Dans la question initiale, nous avions deux niveaux différents d'appels String.Join, donc chacun des appels imbriqués aurait créé une chaîne intermédiaire. En d'autres termes, il est encore plus complexe et plus difficile à deviner. Je serais surpris de voir de toute façon « gagner » de manière significative (en termes de complexité) avec des données typiques.

EDIT: Quand je suis à la maison, je vais écrire un point de référence qui est aussi douloureux que peut-être pour StringBuilder. En gros, si vous avez un tableau où chaque élément est environ deux fois la taille de la précédente, et vous obtenez juste, vous devriez être en mesure de forcer une copie pour chaque append (des éléments, et non du délimiteur, bien que doit être pris en compte aussi). À ce moment-là, il est presque aussi mauvais que concaténation de chaîne simple - mais String.Join aura pas de problème

.

Autres conseils

Voici mon banc d'essai, en utilisant int[][] pour la simplicité; Les premiers résultats:

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

(mise à jour de résultats de double:)

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

(mise à jour 2048 re * 64 * 150)

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

et OptimizeForTesting activé:

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

Alors plus rapide, mais pas massivement si; plate-forme (exécuté à la console, en mode de libération, 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();
        }
    }
}

Je ne pense pas. En regardant à travers réflecteur, la mise en œuvre de String.Join semble très optimisé. Il a aussi l'avantage de connaître la taille totale de la chaîne à créer à l'avance, donc il n'a pas besoin de réaffectation.

J'ai créé deux méthodes d'essai pour les comparer:

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

Je courus chaque procédé 50 fois, en passant dans un tableau de taille [2048][64]. Je l'ai fait pour deux tableaux; un rempli de zéros et un autre rempli avec des valeurs aléatoires. J'ai obtenu les résultats suivants sur ma machine (P4 3.0 GHz, single-core, pas HT, en cours d'exécution en mode de sortie 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

L'augmentation de la taille du tableau à [2048][512], tout en diminuant le nombre d'itérations à 10 m'a donné les résultats suivants:

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

Les résultats sont reproductibles (presque, avec de petites fluctuations causées par des valeurs aléatoires différentes). Apparemment String.Join est un peu plus rapide la plupart du temps (bien que par une très faible marge).

Ceci est le code que j'utilisé pour le test:

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 moins que la différence de 1% se transforme en quelque chose d'important en termes de temps l'ensemble du programme prend à courir, cela ressemble à micro-optimisation. J'écrire le code qui est le plus lisible / compréhensible sans se soucier de la différence de performance de 1%.

Atwood avait une sorte de poste de ce lié à il y a environ un mois:

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

oui. Si vous faites plus que deux jointures, il sera beaucoup plus vite.

Lorsque vous effectuez une string.join, le moteur d'exécution doit:

  1. Allouer mémoire pour la chaîne résultante
  2. copier le contenu de la première chaîne au début de la chaîne de sortie
  3. copier le contenu de la seconde chaîne à la fin de la chaîne de sortie.

Si vous faites deux jointures, il doit copier les données deux fois, et ainsi de suite.

StringBuilder alloue une mémoire tampon avec l'espace à ménager, de sorte que les données peuvent être ajoutés sans avoir à copier la chaîne d'origine. Comme il est l'espace qui reste dans la mémoire tampon, la chaîne jointe peut être écrite dans le tampon directement. Ensuite, il a suffit de copier l'intégralité de la chaîne une fois, à la fin.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top