¿Usar el método de eliminación de StringBuilder es más eficiente en la memoria que crear un nuevo StringBuilder en bucle?

StackOverflow https://stackoverflow.com/questions/266923

Pregunta

En C #, que es más eficiente en memoria: ¿Opción # 1 u Opción # 2?

public void TestStringBuilder()
{
    //potentially a collection with several hundred items:
    string[] outputStrings = new string[] { "test1", "test2", "test3" };

    //Option #1
    StringBuilder formattedOutput = new StringBuilder();
    foreach (string outputString in outputStrings)
    {
        formattedOutput.Append("prefix ");
        formattedOutput.Append(outputString);
        formattedOutput.Append(" postfix");

        string output = formattedOutput.ToString();
        ExistingOutputMethodThatOnlyTakesAString(output);

        //Clear existing string to make ready for next iteration:
        formattedOutput.Remove(0, output.Length);
    }

    //Option #2
    foreach (string outputString in outputStrings)
    {
        StringBuilder formattedOutputInsideALoop = new StringBuilder();

        formattedOutputInsideALoop.Append("prefix ");
        formattedOutputInsideALoop.Append(outputString);
        formattedOutputInsideALoop.Append(" postfix");

        ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());
    }
}

private void ExistingOutputMethodThatOnlyTakesAString(string output)
{
    //This method actually writes out to a file.
    System.Console.WriteLine(output);
}
¿Fue útil?

Solución

Varias de las respuestas sugirieron gentilmente que me quitara de encima y lo descubriera yo mismo, así que a continuación están mis resultados. Creo que el sentimiento generalmente va en contra de este sitio, pero si quieres que algo se haga bien, también podrías ... :)

Modifiqué la opción # 1 para aprovechar la sugerencia de @Ty para usar StringBuilder.Length = 0 en lugar del método Remove. Esto hizo que el código de las dos opciones sea más similar. Las dos diferencias ahora son si el constructor para el StringBuilder está dentro o fuera del ciclo y la opción # 1 ahora usa el método Longitud para borrar el StringBuilder. Ambas opciones se configuraron para ejecutarse en una matriz outputStrings con 100,000 elementos para hacer que el recolector de basura haga algún trabajo.

Un par de respuestas ofrecieron sugerencias para mirar los diversos contadores PerfMon & amp; tal y usar los resultados para elegir una opción. Investigué un poco y terminé usando el Explorador de rendimiento integrado de la edición Visual Studio Team Systems Developer que tengo en el trabajo. Encontré la segunda entrada de blog de una serie de varias partes que explicaba cómo configurarla aquí . Básicamente, conecta una prueba de unidad para señalar el código que desea perfilar; pasar por un asistente y amp; algunas configuraciones; e inicie el perfil de prueba de la unidad. He habilitado la asignación de objetos .NET & amp; métricas de por vida. Los resultados de la creación de perfiles eran difíciles de formatear para esta respuesta, así que los coloqué al final. Si copia y pega el texto en Excel y los masajea un poco, serán legibles.

La opción # 1 es la mayor eficiencia de memoria porque hace que el recolector de basura trabaje un poco menos y asigna la mitad de la memoria y las instancias al objeto StringBuilder que la opción # 2. Para la codificación diaria, elegir la opción n. ° 2 está perfectamente bien.

Si todavía está leyendo, hice esta pregunta porque la Opción # 2 hará que los detectores de pérdida de memoria de un desarrollador de C / C ++ experimentado se vuelvan balísticos. Se producirá una pérdida de memoria enorme si la instancia de StringBuilder no se libera antes de reasignarse. Por supuesto, los desarrolladores de C # no nos preocupamos por esas cosas (hasta que saltan y nos muerden). ¡Gracias a todos!


ClassName   Instances   TotalBytesAllocated Gen0_InstancesCollected Gen0BytesCollected  Gen1InstancesCollected  Gen1BytesCollected
=======Option #1                    
System.Text.StringBuilder   100,001 2,000,020   100,016 2,000,320   2   40
System.String   301,020 32,587,168  201,147 11,165,268  3   246
System.Char[]   200,000 8,977,780   200,022 8,979,678   2   90
System.String[] 1   400,016 26  1,512   0   0
System.Int32    100,000 1,200,000   100,061 1,200,732   2   24
System.Object[] 100,000 2,000,000   100,070 2,004,092   2   40
======Option #2                 
System.Text.StringBuilder   200,000 4,000,000   200,011 4,000,220   4   80
System.String   401,018 37,587,036  301,127 16,164,318  3   214
System.Char[]   200,000 9,377,780   200,024 9,379,768   0   0
System.String[] 1   400,016 20  1,208   0   0
System.Int32    100,000 1,200,000   100,051 1,200,612   1   12
System.Object[] 100,000 2,000,000   100,058 2,003,004   1   20

Otros consejos

La opción 2 debería (creo) realmente superar a la opción 1. El acto de llamar a Eliminar " fuerzas " StringBuilder para tomar una copia de la cadena que ya ha sido devuelta. La cadena es realmente mutable dentro de StringBuilder, y StringBuilder no toma una copia a menos que sea necesario. Con la opción 1, copia antes de borrar básicamente la matriz, con la opción 2 no se requiere copia.

El único inconveniente de la opción 2 es que si la cadena termina siendo larga, se realizarán varias copias mientras se agregan, mientras que la opción 1 mantiene el tamaño original del búfer. Sin embargo, si este va a ser el caso, especifique una capacidad inicial para evitar la copia adicional. (En su código de muestra, la cadena terminará siendo más grande que los 16 caracteres predeterminados; inicializarla con una capacidad de, digamos, 32 reducirá las cadenas adicionales requeridas).

Aparte del rendimiento, sin embargo, la opción 2 es más limpia.

Mientras realiza la creación de perfiles, también puede intentar establecer la longitud de StringBuilder en cero cuando ingresa al bucle.

formattedOutput.Length = 0;

Dado que solo le preocupa la memoria, le sugiero:

foreach (string outputString in outputStrings)
    {    
        string output = "prefix " + outputString + " postfix";
        ExistingOutputMethodThatOnlyTakesAString(output)  
    }

La salida denominada variable tiene el mismo tamaño en su implementación original, pero no se necesitan otros objetos. StringBuilder utiliza cadenas y otros objetos internamente y se crearán muchos objetos que deben ser GC'd.

Tanto la línea de la opción 1:

string output = formattedOutput.ToString();

Y la línea de la opción 2:

ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());

creará un objeto inmutable con el valor del prefijo + outputString + postfix. Esta cadena es del mismo tamaño, sin importar cómo la cree. Lo que realmente está preguntando es qué memoria es más eficiente:

    StringBuilder formattedOutput = new StringBuilder(); 
    // create new string builder

o

    formattedOutput.Remove(0, output.Length); 
    // reuse existing string builder

Saltar el StringBuilder por completo será más eficiente en memoria que cualquiera de los anteriores.

Si realmente necesita saber cuál de los dos es más eficiente en su aplicación (esto probablemente variará según el tamaño de su lista, prefijo y outputStrings) recomendaría el ANTS Profiler de puerta roja http://www.red-gate.com/products/ants_profiler/index.htm

Jason

Odio decirlo, pero ¿qué tal solo probarlo?

Estas cosas son fáciles de descubrir por ti mismo. Ejecute Perfmon.exe y agregue un contador para .NET Memory + Gen 0 Collections. Ejecute el código de prueba un millón de veces. Verá que la opción 1 requiere la mitad de la cantidad de colecciones que necesita la opción 2.

Hemos hemos hablado de esto antes con Java , aquí está los resultados [Release] de la versión C #:

Option #1 (10000000 iterations): 11264ms
Option #2 (10000000 iterations): 12779ms

Actualización: en mi análisis no científico, permitir que los dos métodos se ejecuten mientras se monitorea todos los contadores de rendimiento de memoria en perfmon no resultó en ningún tipo de diferencia discernible con ninguno de los métodos (aparte de tener algunos contadores pico solo mientras cualquiera de las pruebas era ejecutando).

Y esto es lo que solía probar:

class Program
{
    const int __iterations = 10000000;

    static void Main(string[] args)
    {
        TestStringBuilder();
        Console.ReadLine();
    }

    public static void TestStringBuilder()
    {
        //potentially a collection with several hundred items:
        var outputStrings = new [] { "test1", "test2", "test3" };

        var stopWatch = new Stopwatch();

        //Option #1
        stopWatch.Start();
        var formattedOutput = new StringBuilder();

        for (var i = 0; i < __iterations; i++)
        {
            foreach (var outputString in outputStrings)
            {
                formattedOutput.Append("prefix ");
                formattedOutput.Append(outputString);
                formattedOutput.Append(" postfix");

                var output = formattedOutput.ToString();
                ExistingOutputMethodThatOnlyTakesAString(output);

                //Clear existing string to make ready for next iteration:
                formattedOutput.Remove(0, output.Length);
            }
        }
        stopWatch.Stop();

        Console.WriteLine("Option #1 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
            Console.ReadLine();
        stopWatch.Reset();

        //Option #2
        stopWatch.Start();
        for (var i = 0; i < __iterations; i++)
        {
            foreach (var outputString in outputStrings)
            {
                StringBuilder formattedOutputInsideALoop = new StringBuilder();

                formattedOutputInsideALoop.Append("prefix ");
                formattedOutputInsideALoop.Append(outputString);
                formattedOutputInsideALoop.Append(" postfix");

                ExistingOutputMethodThatOnlyTakesAString(
                   formattedOutputInsideALoop.ToString());
            }
        }
        stopWatch.Stop();

        Console.WriteLine("Option #2 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
    }

    private static void ExistingOutputMethodThatOnlyTakesAString(string s)
    {
        // do nothing
    }
} 

La opción 1 en este escenario es marginalmente más rápida, aunque la opción 2 es más fácil de leer y mantener. A menos que esté realizando esta operación millones de veces consecutivas, me quedaría con la Opción 2 porque sospecho que las opciones 1 y 2 son casi iguales cuando se ejecuta en una sola iteración.

Yo diría la opción # 2 si definitivamente es más directo. En términos de rendimiento, suena como algo que solo necesitarías probar y ver. Supongo que no hace la diferencia suficiente elegir la opción menos sencilla.

Creo que la opción 1 sería un poco más de memoria eficiente ya que no se crea un nuevo objeto cada vez. Dicho esto, el GC hace un buen trabajo limpiando recursos como en la opción 2.

Creo que puede estar cayendo en la trampa de la optimización prematura ( la raíz de todo mal - -Knuth ). Tu IO tomará muchos más recursos que el generador de cadenas.

Tiendo a ir con la opción más clara / limpiadora, en este caso la opción 2.

Rob

  1. Mídelo
  2. Preasigne lo más cerca posible de la cantidad de memoria que cree que necesitará
  3. Si la velocidad es su preferencia, entonces considere un enfoque concurrente de varios hilos de frente a medio, de medio a extremo bastante sencillo (expanda la división del trabajo según sea necesario)
  4. medirlo de nuevo

¿Qué es más importante para ti?

  1. memoria

  2. velocidad

  3. clarity

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top