String.Join vs. StringBuilder: ¿cuál es más rápido?
En una pregunta anterior sobre cómo formatear un archivo double[][]
en formato CSV, se sugirió que usar StringBuilder
sería más rápido que String.Join
. ¿Es esto cierto?
Respuesta corta: depende.
Respuesta larga: si ya tiene una serie de cadenas para concatenar (con un delimitador), String.Join
es la forma más rápida de hacerlo.
String.Join
Puede revisar todas las cadenas para determinar la longitud exacta que necesita, luego volver a copiar todos los datos. Esto significa que no habrá copias adicionales involucradas. El único inconveniente es que tiene que revisar las cadenas dos veces, lo que significa potencialmente agotar la memoria caché más veces de las necesarias.
Si no tiene las cadenas como una matriz de antemano, probablemente sea más rápido de usar StringBuilder
, pero habrá situaciones en las que no lo será. Si usa un StringBuilder
medio para hacer muchísimas copias, entonces construir una matriz y luego llamar String.Join
puede ser más rápido.
EDITAR: Esto es en términos de una sola llamada a String.Join
versus un montón de llamadas a StringBuilder.Append
. En la pregunta original, teníamos dos niveles diferentes de String.Join
llamadas, por lo que cada una de las llamadas anidadas habría creado una cadena intermedia. En otras palabras, es aún más complejo y difícil de adivinar. Me sorprendería ver que de cualquier manera "ganar" significativamente (en términos de complejidad) con datos típicos.
EDITAR: Cuando esté en casa, escribiré un punto de referencia que sea lo más doloroso posible para StringBuilder
. Básicamente, si tiene una matriz donde cada elemento tiene aproximadamente el doble del tamaño del anterior y lo hace bien, debería poder forzar una copia para cada anexo (de elementos, no del delimitador, aunque eso debe ser necesario). tener en cuenta también). En ese punto, es casi tan malo como una simple concatenación de cadenas, pero String.Join
no tendrá problemas.
Aquí está mi equipo de prueba, usado int[][]
por simplicidad; resultados primero:
Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000
(actualización para double
resultados :)
Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000
(actualización re 2048 * 64 * 150)
Join: 11620ms (chk: 206409600
OneBuilder: 11132ms (chk: 206409600
y con OptimizeForTesting habilitado:
Join: 11180ms (chk: 206409600
OneBuilder: 10784ms (chk: 206409600
Muy más rápido, pero no enormemente; 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();
}
}
}
No me parece. Mirando a través de Reflector, la implementación de String.Join
parece muy optimizada. También tiene el beneficio adicional de conocer de antemano el tamaño total de la cadena que se creará, por lo que no necesita ninguna reasignación.
He creado dos métodos de prueba para compararlos:
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();
}
Ejecuté cada método 50 veces, pasando una matriz de tamaño [2048][64]
. Hice esto para dos matrices; uno lleno de ceros y otro lleno de valores aleatorios. Obtuve los siguientes resultados en mi máquina (P4 3,0 GHz, un solo núcleo, sin HT, ejecutando el modo de lanzamiento desde 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
Al aumentar el tamaño de la matriz a [2048][512]
, mientras se disminuía el número de iteraciones a 10, obtuve 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). Aparentemente String.Join
es un poco más rápido la mayor parte del tiempo (aunque por un margen muy pequeño).
Este es el código que utilicé 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));
}
}