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?
Réponses
Trop de publicités?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.
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();
}
}
}
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));
}
}