Ssortingng.Join vs. SsortingngBuilder: qui est plus rapide?

Dans une question précédente sur le formatage d’un double[][] au format CSV, Marc Gravell a déclaré que l’utilisation de SsortingngBuilder serait plus rapide que Ssortingng.Join . Est-ce vrai?

Réponse courte: ça dépend.

Réponse longue: si vous avez déjà un tableau de chaînes à concaténer (avec un délimiteur), Ssortingng.Join est le moyen le plus rapide de le faire.

Ssortingng.Join peut parcourir toutes les chaînes pour déterminer la longueur exacte Ssortingng.Join , puis recommencer et copier toutes les données. Cela signifie qu’il n’y aura pas de copie supplémentaire impliquée. Le seul inconvénient est qu’il doit parcourir les chaînes deux fois, ce qui signifie que le cache de la mémoire risque d’être multiplié par le nombre de fois nécessaire.

Si vous n’avez pas les chaînes en tant que tableau au préalable, il est probablement plus rapide d’utiliser SsortingngBuilder – mais il y aura des situations où ce ne sera pas le cas. Si vous utilisez un SsortingngBuilder , cela signifie qu’il faut faire beaucoup de copies, puis créer un tableau puis appeler Ssortingng.Join peut être plus rapide.

EDIT: Ceci est en termes d’un seul appel à Ssortingng.Join vs un tas d’appels à SsortingngBuilder.Append . Dans la question initiale, nous avions deux niveaux d’appels Ssortingng.Join différents, de sorte que chacun des appels nesteds aurait créé une chaîne intermédiaire. En d’autres termes, il est encore plus complexe et 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 chez moi, je vais écrire un test aussi pénible que possible pour SsortingngBuilder . Fondamentalement, si vous avez un tableau où chaque élément est environ deux fois plus grand que le précédent, vous devriez pouvoir en forcer une copie pour chaque append (des éléments, pas du délimiteur, bien que être pris en compte aussi). À ce stade, c’est presque aussi grave que la simple concaténation de chaînes – mais Ssortingng.Join n’aura aucun problème.

Voici mon outil de test, en utilisant int[][] pour plus de simplicité; résultats d’abord:

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

(mise à jour pour double résultats double 🙂

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

(mise à jour concernant 2048 * 64 * 150)

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

et avec OptimizeForTesting activé:

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

Si vite, mais pas massivement, rig (exécuter à 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(ssortingng[] 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 => Ssortingng.Join(",", Array.ConvertAll(row, x => x.ToSsortingng())))); } public static ssortingng OneBuilder(IEnumerable source) { SsortingngBuilder sb = new SsortingngBuilder(); 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 Reflector, l’implémentation de Ssortingng.Join semble très optimisée. Il présente également l’avantage de connaître la taille totale de la chaîne à créer à l’avance, de sorte qu’il ne nécessite aucune réallocation.

J’ai créé deux méthodes de test pour les comparer:

 public static ssortingng TestSsortingngJoin(double[][] array) { return Ssortingng.Join(Environment.NewLine, Array.ConvertAll(array, row => Ssortingng.Join(",", Array.ConvertAll(row, x => x.ToSsortingng())))); } public static ssortingng TestSsortingngBuilder(double[][] source) { // based on Marc Gravell's code SsortingngBuilder sb = new SsortingngBuilder(); 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 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 de valeurs aléatoires. J'ai obtenu les résultats suivants sur ma machine (P4 3.0 GHz, single-core, pas de HT, en mode Release de CMD):

 // with zeros: TestSsortingngJoin took 00:00:02.2755280 TestSsortingngBuilder took 00:00:02.3536041 // with random values: TestSsortingngJoin took 00:00:05.6412147 TestSsortingngBuilder took 00:00:05.8394650 

En augmentant la taille du tableau à [2048][512] , tout en diminuant le nombre d'itérations à 10, j'ai obtenu les résultats suivants:

 // with zeros: TestSsortingngJoin took 00:00:03.7146628 TestSsortingngBuilder took 00:00:03.8886978 // with random values: TestSsortingngJoin took 00:00:09.4991765 TestSsortingngBuilder took 00:00:09.3033365 

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

C'est le code que j'ai utilisé pour tester:

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

À moins que la différence de 1% ne se transforme en quelque chose de significatif en termes de temps nécessaire à l’exécution du programme, cela ressemble à une micro-optimisation. J’écrirais le code le plus lisible / compréhensible et ne m’inquiéterais pas de la différence de performance de 1%.

Atwood avait un post de ce genre il y a environ un mois:

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

Oui. Si vous faites plus que quelques jointures, ce sera beaucoup plus rapide.

Lorsque vous faites un ssortingng.join, le runtime doit:

  1. Allouer de la 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 deuxième chaîne à la fin de la chaîne de sortie.

Si vous effectuez deux jointures, il doit copier les données deux fois, etc.

SsortingngBuilder alloue un tampon avec un espace disponible afin que les données puissent être ajoutées sans avoir à copier la chaîne d’origine. Comme il rest de la place dans le tampon, la chaîne ajoutée peut être écrite directement dans le tampon. Ensuite, il suffit de copier la chaîne entière une fois, à la fin.