L'uso del metodo StringBuilder Remove è più efficiente della memoria rispetto alla creazione di un nuovo StringBuilder in loop?

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

Domanda

In C # che è più efficiente in termini di memoria: Opzione n. 1 o Opzione 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);
}
È stato utile?

Soluzione

Molte delle risposte hanno suggerito delicatamente di togliermi dal duff e di capirlo da solo, quindi di seguito sono riportati i miei risultati. Penso che il sentimento generalmente vada contro il grano di questo sito ma se vuoi qualcosa di giusto, potresti anche farlo .... :)

Ho modificato l'opzione 1 per sfruttare il suggerimento di @Ty di utilizzare StringBuilder.Length = 0 invece del metodo Remove. Ciò ha reso il codice delle due opzioni più simile. Le due differenze sono ora se il costruttore di StringBuilder è dentro o fuori dal ciclo e l'opzione # 1 ora utilizza il metodo Length per cancellare StringBuilder. Entrambe le opzioni sono state impostate per funzionare su un array outputStrings con 100.000 elementi per fare in modo che il Garbage Collector funzioni un po '.

Un paio di risposte hanno offerto suggerimenti per esaminare i vari contatori e ampli PerfMon; tale e utilizzare i risultati per selezionare un'opzione. Ho fatto qualche ricerca e ho finito per utilizzare Performance Explorer integrato dell'edizione Developer Studio di Visual Studio Team Systems che ho al lavoro. Ho trovato il secondo post di blog di una serie multipart che spiega come configurarlo qui . Fondamentalmente, si collega un test unitario per puntare al codice che si desidera profilare; passare attraverso un wizard & amp; alcune configurazioni; e avviare la profilazione dell'unità di prova. Ho abilitato l'allocazione degli oggetti .NET & amp; metriche della vita. I risultati della profilazione sono stati difficili da formattare per questa risposta, quindi li ho inseriti alla fine. Se copi e incolli il testo in Excel e li massaggi un po ', saranno leggibili.

L'opzione n. 1 è la maggiore efficienza della memoria perché consente al garbage collector di svolgere un po 'meno lavoro e alloca metà della memoria e delle istanze sull'oggetto StringBuilder rispetto all'opzione n. 2. Per la codifica quotidiana, l'opzione di selezione n. 2 va benissimo.

Se stai ancora leggendo, ho posto questa domanda perché l'opzione n. 2 renderà balistici i rilevatori di perdite di memoria di uno sviluppatore C / C ++ di esperienza. Si verificherà un'enorme perdita di memoria se l'istanza StringBuilder non viene rilasciata prima di essere riassegnata. Naturalmente, noi sviluppatori C # non ci preoccupiamo di queste cose (fino a quando non saltano su e ci mordono). Grazie a tutti !!


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

Altri suggerimenti

L'opzione 2 dovrebbe (credo) effettivamente sovraperformare l'opzione 1. L'atto di chiamare Rimuovi " forza " StringBuilder per prendere una copia della stringa già restituita. La stringa è effettivamente mutabile in StringBuilder e StringBuilder non ne prende una copia a meno che non sia necessario. Con l'opzione 1 viene copiato prima di cancellare sostanzialmente l'array - con l'opzione 2 non è richiesta alcuna copia.

L'unico aspetto negativo dell'opzione 2 è che se la stringa finisce per essere lunga, ci saranno più copie fatte durante l'aggiunta - mentre l'opzione 1 mantiene la dimensione originale del buffer. In tal caso, tuttavia, specificare una capacità iniziale per evitare la copia aggiuntiva. (Nel tuo codice di esempio, la stringa finirà per essere più grande dei 16 caratteri predefiniti - inizializzandola con una capacità, diciamo, 32 ridurrà le stringhe extra richieste.)

A parte le prestazioni, tuttavia, l'opzione 2 è solo più pulita.

Durante la creazione del profilo, puoi anche provare a impostare la lunghezza di StringBuilder su zero quando entri nel ciclo.

formattedOutput.Length = 0;

Dato che ti preoccupi solo della memoria, suggerirei:

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

La variabile denominata output ha le stesse dimensioni dell'implementazione originale, ma non sono necessari altri oggetti. StringBuilder utilizza stringhe e altri oggetti internamente e verranno creati molti oggetti che devono essere GC'd.

Sia la riga dell'opzione 1:

string output = formattedOutput.ToString();

E la linea dall'opzione 2:

ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());

creerà un oggetto immutabile con il valore del prefisso + outputString + postfix. Questa stringa ha le stesse dimensioni, indipendentemente da come la crei. Quello che stai veramente chiedendo è quale sia il più efficiente in termini di memoria:

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

o

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

Saltare completamente StringBuilder sarà più efficiente in termini di memoria rispetto a uno dei precedenti.

Se hai davvero bisogno di sapere quale dei due è più efficiente nella tua applicazione (questo probabilmente varierà in base alla dimensione della tua lista, prefisso e outputStrings) ti consiglierei Red-gate ANTS Profiler http://www.red-gate.com/products/ants_profiler/index.htm

Jason

Odio dirlo, ma che ne dici di testarlo?

Questa roba è facile da scoprire da solo. Esegui Perfmon.exe e aggiungi un contatore per le raccolte .NET Memory + Gen 0. Esegui il codice di test un milione di volte. Vedrai che l'opzione n. 1 richiede la metà del numero di raccolte richieste dall'opzione n. 2.

Ne abbiamo di cui abbiamo già parlato con Java , ecco i risultati [Release] della versione C #:

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

Aggiornamento: nella mia analisi non scientifica, consentire l'esecuzione dei due metodi durante il monitoraggio di tutti i contatori delle prestazioni della memoria in perfmon non ha comportato alcuna differenza distinguibile con nessuno dei due metodi (a parte il fatto che alcuni picchi di contatori sono stati eseguiti solo mentre uno dei test era esecuzione).

Ed ecco cosa ho usato per testare:

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

L'opzione 1 in questo scenario è leggermente più veloce sebbene l'opzione 2 sia più facile da leggere e mantenere. A meno che tu non stia eseguendo questa operazione milioni di volte back to back, rimarrei con l'opzione 2 perché sospetto che le opzioni 1 e 2 siano quasi uguali quando si esegue in una singola iterazione.

Direi l'opzione n. 2 se decisamente più semplice. In termini di prestazioni, sembra qualcosa che dovresti solo provare e vedere. Immagino che non faccia abbastanza differenza per scegliere l'opzione meno semplice.

Penso che l'opzione 1 sarebbe leggermente più memoria efficiente poiché un nuovo oggetto non viene creato ogni volta. Detto questo, il GC fa un ottimo lavoro nel ripulire le risorse come nell'opzione 2.

Penso che potresti cadere nella trappola dell'ottimizzazione prematura ( la radice di tutti i mali - -Knuth). Il tuo IO impiegherà molte più risorse del generatore di stringhe.

Tendo ad andare con l'opzione più chiara / più pulita, in questo caso l'opzione 2.

Rob

  1. Misuralo
  2. Pre-alloca il più vicino possibile a quanta memoria pensi di aver bisogno
  3. Se la velocità è la tua preferenza, prendi in considerazione un approccio simultaneo multi-thread da fronte a medio, da medio a fine (espandi la divisione del lavoro secondo necessità)
  4. misuralo di nuovo

che cosa è più importante per te?

  1. memoria

  2. velocità

  3. chiarezza

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top