94 votes

String.Join vs. StringBuilder: qui est plus rapide?

Dans une question précédente sur la mise en forme d'un double[][] au format CSV, Marc Gravell a déclaré que l'utilisation de StringBuilder serait plus rapide que String.Join . Est-ce vrai?

130voto

Jon Skeet Points 692016

Réponse courte: ça dépend.

Réponse longue: si vous avez déjà un tableau de chaînes de caractères pour concaténer (avec un délimiteur), Chaîne de caractères.Rejoindre est le moyen le plus rapide de le faire.

Chaîne de caractères.Jointure peut regarder toutes les chaînes de travailler sur la longueur exacte dont il a besoin, puis aller de nouveau et de copier toutes les données. Cela signifie qu'il y aura pas de supplément de reproduction. Le seul inconvénient est qu'il doit passer par les cordes deux fois, ce qui signifie que, potentiellement, de soufflage de la mémoire cache plus de fois que nécessaire.

Si vous n'avez pas les chaînes comme un tableau à l'avance, c'est probablement plus rapide à utiliser StringBuilder - mais il y aura des situations où il n'est pas. Si vous utilisez un StringBuilder est à dire faire des tas et des tas de copies, puis la construction d'un tableau et puis l'appel de la Chaîne.Rejoignez pourrait bien être plus rapide.

EDIT: C'est en termes d'un seul appel à la Chaîne.Rejoignez vs un tas d'appels à StringBuilder.Append. Dans la question d'origine, nous avons eu deux différents niveaux de la Chaîne.Joindre les appels, de sorte que chacun des appels imbriqués ont créé une intermédiaire de la chaîne. En d'autres termes, il est encore plus complexe et plus difficile à deviner. Je serais surpris de le voir de toute façon de "gagner" sensiblement (dans la complexité) avec des données typiques.

EDIT: Quand je suis chez moi, je vais écrire un indice 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 d'environ deux fois la taille de la précédente, et que vous obtenez juste, vous devriez être en mesure de forcer une copie pour chaque append (des éléments, pas de délimiteur, bien que cela doit être pris en compte). À ce stade, c'est presque aussi mauvais que le simple concaténation de chaîne - mais de Chaîne.Rejoignez aura pas de problèmes.

36voto

Marc Gravell Points 482669

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

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

(mise à jour pour double résultats :)

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

(mise à jour re 2048 * 64 * 150)

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

et avec OptimizeForTesting activé:

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

Tellement plus vite, mais pas massivement; rig (exécuté sur console, en mode release, 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();
        }
    }
}
 

24voto

Hosam Aly Points 14797

Je ne le pense pas. En regardant à travers le Réflecteur, la mise en œuvre de l' String.Join semble très optimisé. Il a aussi l'avantage de connaître le total

taille de la chaîne à être créé à l'avance, de sorte qu'il n'a pas besoin de toute réaffectation.

J'ai créé deux méthodes de test pour 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();
}

J'ai couru à chaque méthode de 50 fois, en passant un tableau de taille [2048][64]. Je l'ai fait pour les deux tableaux, l'un rempli avec des zéros et un autre rempli avec de l'aléatoire

des valeurs. J'ai obtenu les résultats suivants sur ma machine (P4 3.0 GHz, single-core, pas de HT, l'exécution de mode de lancement 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 de la matrice d' [2048][512], tout en diminuant le nombre d'itérations à 10 m'a obtenu 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 les différentes valeurs aléatoires). Apparemment String.Join est un peu plus rapide que la plupart du temps (bien que par une très petite marge).

C'est le code que j'ai 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));
    }
}

14voto

tvanfosson Points 268301

À moins que la différence de 1% ne devienne un facteur significatif en termes de temps nécessaire à l'exécution du programme, cela ressemble à de la micro-optimisation. J'écrirais le code le plus lisible / compréhensible sans me soucier de la différence de performance de 1%.

-1voto

Adam Neal Points 1649

Atwood avait un genre de message lié à cela il y a environ un mois:

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X