2011-02-09 9 views
12

Stavo giocando con C# e volevo accelerare un programma. Ho apportato delle modifiche e sono riuscito a farlo. Tuttavia, ho bisogno di aiuto per capire perché il cambiamento lo ha reso più veloce.Aiuto alla comprensione dell'ottimizzazione C#

Ho tentato di ridurre il codice a qualcosa di più facile da capire in una domanda. Score1 e Report1 sono il modo più lento. Score2 e Report2 è il modo più veloce. Il primo metodo memorizza dapprima una stringa e un int in una struct in parallelo. Successivamente, in un ciclo seriale, scorre in una matrice di quelle strutture e scrive i loro dati in un buffer. Il secondo metodo scrive prima i dati in un buffer di stringhe in parallelo. Successivamente, in un ciclo seriale, scrive i dati della stringa in un buffer. Qui ci sono alcuni tempi di esecuzione del campione:

Run 1 Totale Media Time = 0,492,087 mila sec Run 2 Tempo complessivo medio = 0,273,619 mila sec

Quando lavoravo con una versione precedente non paralleli di questo, i tempi erano quasi lo stesso. Perché la differenza con la versione parallela?

Anche se riduco il loop in Report1 per scrivere una singola riga di output nel buffer, è ancora più lento (tempo totale circa, 42 secondi).

Ecco il codice semplificato.

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Diagnostics; 
using System.Threading.Tasks; 
using System.IO; 

namespace OptimizationQuestion 
{ 
    class Program 
    { 
     struct ValidWord 
     { 
      public string word; 
      public int score; 
     } 
     ValidWord[] valid; 
     StringBuilder output; 
     int total; 

     public void Score1(string[] words) 
     { 
      valid = new ValidWord[words.Length]; 

      for (int i = 0; i < words.Length; i++) 
      { 
       StringBuilder builder = new StringBuilder(); 

       foreach (char c in words[i]) 
       { 
        if (c != 'U') 
         builder.Append(c); 
       } 
       if (words[i].Length == 3) 
       { 
        valid[i] = new ValidWord 
        { word = builder.ToString(), score = words[i].Length }; 
       } 
      } 
     } 
     public void Report1(StringBuilder outputBuffer) 
     { 
      int total = 0; 
      foreach (ValidWord wordInfo in valid) 
      { 
       if (wordInfo.score > 0) 
       { 
        outputBuffer.AppendLine(String.Format("{0} {1}", wordInfo.word.ToString(), wordInfo.score)); 
        total += wordInfo.score; 
       } 
      } 
      outputBuffer.AppendLine(string.Format("Total = {0}", total)); 
     } 

     public void Score2(string[] words) 
     { 
      output = new StringBuilder(); 
      total = 0;   
      for (int i = 0; i < words.Length; i++) 
      { 
       StringBuilder builder = new StringBuilder(); 

       foreach (char c in words[i]) 
       { 
        if (c != 'U') 
         builder.Append(c); 
       } 
       if (words[i].Length == 3) 
       { 
        output.AppendLine(String.Format("{0} {1}", builder.ToString(), words[i].Length)); 
        total += words[i].Length; 
       } 
      } 
     } 
     public void Report2(StringBuilder outputBuffer) 
     { 
      outputBuffer.Append(output.ToString()); 
      outputBuffer.AppendLine(string.Format("Total = {0}", total)); 
     } 
     static void Main(string[] args) 
     { 
      Program[] program = new Program[100]; 
      for (int i = 0; i < program.Length; i++) 
       program[i] = new Program(); 

      string[] words = File.ReadAllLines("words.txt"); 

      Stopwatch stopwatch = new Stopwatch(); 
      const int TIMING_REPETITIONS = 20; 
      double averageTime1 = 0.0; 
      StringBuilder output = new StringBuilder(); 
      for (int i = 0; i < TIMING_REPETITIONS; ++i) 
      { 
       stopwatch.Reset(); 
       stopwatch.Start(); 
       output.Clear(); 
       Parallel.ForEach<Program>(program, p => 
        { 
         p.Score1(words); 
        }); 
       for (int k = 0; k < program.Length; k++) 
        program[k].Report1(output); 
       stopwatch.Stop(); 
       averageTime1 += stopwatch.Elapsed.TotalSeconds; 
       GC.Collect(); 
      } 
      averageTime1 /= (double)TIMING_REPETITIONS; 
      Console.WriteLine(string.Format("Run 1 Total Average Time = {0:0.000000} sec", averageTime1)); 
      double averageTime2 = 0.0; 
      for (int i = 0; i < TIMING_REPETITIONS; ++i) 
      { 
       stopwatch.Reset(); 
       stopwatch.Start(); 
       output.Clear(); 
       Parallel.ForEach<Program>(program, p => 
        { 
         p.Score2(words); 
        }); 
       for (int k = 0; k < program.Length; k++) 
        program[k].Report2(output); 
       stopwatch.Stop(); 
       averageTime2 += stopwatch.Elapsed.TotalSeconds; 
       GC.Collect(); 
      } 
      averageTime2 /= (double)TIMING_REPETITIONS; 
      Console.WriteLine(string.Format("Run 2 Total Average Time = {0:0.000000} sec", averageTime2)); 
      Console.ReadLine(); 
     } 
    } 
} 
+5

Perché stai cercando di rango come codice diverso come Report1 e Report2? Report1 contiene un loop e Report2 no. Forse nella versione non parallela il compilatore C# ha srotolato il loop o qualche altra magia? – Earlz

+0

Ridurre il loop Report1 a una iterazione aiuta un po '(.42 sec), ma dopo la pubblicazione, penso che sia l'allocazione dell'array in Score1. – jlim

+1

Nota: l'elenco di parole contiene circa 14.000 righe di stringhe. Quindi ogni chiamata di score1 alloca 14.000 strutture. – jlim

risposta

1

Prima di tutto, si stanno parallelizzando le corse ripetute. Ciò migliorerà il tuo tempo di riferimento, ma potrebbe non essere di grande aiuto per la tua reale durata di produzione. Per misurare con precisione il tempo necessario per eseguire effettivamente un elenco di parole, è necessario avere esattamente un elenco di parole alla volta. In caso contrario, i singoli thread che elaborano tutti gli elenchi sono in qualche modo in concorrenza tra loro per le risorse di sistema e il tempo per elenco soffre, anche se il tempo per eseguire tutte le liste in totale è più veloce.

Per accelerare il tempo di elaborazione di un elenco di parole, si desidera elaborare le singole parole nell'elenco in parallelo, per esattamente un elenco alla volta. Per ottenere una definizione/dimensione sufficiente per una buona misurazione, rendere l'elenco molto lungo o elaborare l'elenco più volte in serie.

Nel tuo caso, questo diventa un po 'complicato perché il costruttore di stringhe necessario per il tuo prodotto finale non è documentato come thread-safe. Non è poi così male, comunque. Ecco un esempio di chiamare foreach parallelo per una lista unica parola:

var locker = new Object(); //I'd actually make this static, but it should end up as a closure and so still work 
var OutputBuffer = new StringBuilder(); // you can improve things futher if you can make a good estimate for the final size and force it allocate all the memory it will need up front 
int score = 0; 
Parallel.ForEach(words, w => 
{ 
    // We want to push as much of the work to the individual threads as possible. 
    // If run in 1 thread, a stringbuilder per word would be bad. 
    // Run in parallel, it allows us to do a little more of the work outside of locked code. 
    var buf = new StringBuilder(w.Length + 5); 
    string word = buf.Append(w.Where(c=>c!='U').Concat(' ').ToArray()).Append(w.Length).ToString(); 

    lock(locker) 
    { 
     OutputBuffer.Append(word); 
     score += w.Length; 
    } 
}); 
OutputBuffer.Append("Total = ").Append(score); 

basta chiamare che 20 volte in un modo sequenziale normale trattati per ciclo. Ancora una volta, potrebbe finire i benchmark un po 'più lentamente, ma penso che si realizzerà un po' più velocemente nel mondo reale a causa di un difetto nel tuo benchmark. Nota anche che ho digitato questo diritto nella finestra di risposta — Non ho mai provato un evento per compilarlo, e quindi non è probabile che sia perfetto fin dal primo istante.

Dopo aver fissato il punto di riferimento per riflettere più accuratamente come parallelo codice avrà un impatto il tempo di elaborazione del mondo reale, il passo successivo è quello di fare un po 'profiling per vedere dove il programma è in realtà la spesa è il momento. Questo è il modo in cui sai quali aree guardare per migliorare.

Per curiosità, mi piacerebbe anche sapere come questa versione esegue:

var agg = new {score = 0, OutputBuffer = new StringBuilder()}; 
agg = words.Where(w => w.Length == 3) 
    .Select(w => new string(w.Where(c => c!='U').ToArray()) 
    .Aggregate(agg, (a, w) => {a.OutputBuffer.AppendFormat("{0} {1}\n", w, w.Length); score += w.Length;}); 
agg.OutputBuffer.Append("Total = ").Append(score); 
+0

Apprezzo il codice. – jlim

1

La dimensione di una struttura solitamente dovrebbe essere inferiore a quella di un puntatore (se le prestazioni sono il problema principale Microsoft says che qualcosa di meno di 16 byte esegui meglio come struct se la semantica del tipo di riferimento non è necessaria), altrimenti il ​​sovraccarico per il suo superamento aumenta (perché è passato per valore) e sarebbe più di quanto sarebbe stato per il semplice passaggio di un puntatore. La tua struct contiene un puntatore e un int (rendendolo più di un puntatore), quindi potresti sperimentare un sovraccarico a causa di questo.

Vedere il Quando utilizzare le strutture sezione di this article.

+0

* La dimensione di una struttura in genere deve essere inferiore a quella di un puntatore (Microsoft consiglia non più di 16 byte) * - Un riferimento in C# non è affatto vicino a 16 byte. Difficile creare un tipo di struct utile che sia inferiore ai bit 32/64 (+ alcuni overhead di housekeeping) richiesti da un riferimento. –

+0

@Ed S: ecco perché è solo una raccomandazione e non un'applicazione. Le tue strutture potrebbero essere più grandi, tenendo a mente solo questo ha un impatto sulle prestazioni (che dovresti valutare e prendere in considerazione al momento di decidere su una struttura o una classe come Microsoft ha con molte di quelle strutture). – Cornelius

+2

Ok, ma è una brutta raccomandazione. Intendo davvero, per favore mostrami una struttura utile che è <= la dimensione di un riferimento. Con la tua raccomandazione ogni tipo di struct nel framework fallisce, quindi non sono sicuro di come si possa dire che sarebbe "tipico". –

-1

Solo un'idea: non ho fatto alcuna misura, ma per esempio questa linea:

foreach (char c in words[i]) 
  1. penso che sarebbe meglio il creare una variabile temporanea per la parola corrente.

  2. Inoltre, l'iteratore di una stringa potrebbe essere più lento.

Il codice sarebbe diventato qualcosa di simile:

var currentWord = words[i]; 
for (int j = 0; j < currentWord.Length; j++){ 
    char c = currentWord[i]; 
    // ... 
} 

Il nuovo potrebbe anche essere un problema di prestazioni, come qualcuno già individuato. Come ho detto nel mio commento, aggiungere ulteriori dati di profiling aiuterebbe a individuare esattamente cosa sta succedendo. O dare un'occhiata al codice generato.

+6

-1, la versione 'foreach' è ottimizzata specificamente dal compilatore per non calcolare' Length' più e più volte. Un sacco di "penso" e non abbastanza "ho provato" rendono una risposta scarsa. – Blindy

+4

concordato. Indovina su quali ottimizzazioni la compilazione sarà o non verrà eseguita sono completamente inutili. –

+0

Non so perché la gente abbassa così tanto ... Se fai qualche ricerca, vedrai che il codice IL generato da foreach e il for è diverso. (http://abi.exdream.com/Blog/post/2009/04/22/For-vs-Foreach-Performance.aspx Even Oakcool ha pubblicato un link interessante per questo problema). Inoltre, ti sto solo dando alcune idee per aiutarti a indagare sulla questione. Non farò il profiling al tuo posto. – Sauleil

1

Ho provato a eseguirlo tramite un profiler, ma non mi fido dei risultati ottenuti. (Run1 richiede meno tempo rispetto Run2 in esso.) Quindi non ci sono delle risposte concrete lì, ma il mio sospetto è che l'array valida [] è il colpevole:

  1. Questo è un potenzialmente elevato di allocazione di memoria che Run1 sta facendo e Run2 non lo è. L'allocazione di grossi pezzi di memoria può richiedere molto tempo.

  2. È possibile che la matrice si stia allontanando da qualsiasi altro dato di lavoro nella memoria fisica. Per lo meno, è abbastanza grande da finire nell'heap di oggetti di grandi dimensioni, mentre sembra che quasi tutto il resto finirà nello stack o nell'heap di piccoli oggetti. Ciò potrebbe significare che la funzione Score1 sta avendo a che fare con più miss della cache rispetto alla funzione Score2.

Potrebbe trattarsi di un problema molto più piccolo nel codice seriale, in cui è possibile accadere solo una volta in un dato momento. Quando accade per un sacco di thread simultaneamente, però, il problema potrebbe aggravarsi in modo che quello che originariamente ha causato solo un errore di cache extra o due sta causando errori di pagina.

+0

Ho avuto alcuni problemi anche nel profiling del programma originale. Stava puntando a una routine diversa come il collo di bottiglia. Apprezzo l'idea di possibili errori di pagina. – jlim

1

Quindi c'è un post su codeproject che aiuta a rispondere a questo.

http://www.codeproject.com/KB/cs/foreach.aspx

Vi si possono vedere che il codice generato è slitely diverso, quindi in una lunga lista, si perderanno alcuni cicli per quelle poche righe in più, e cambierà il tempo finale.

1

Bene, ho appena sfogliato il codice e i miei primi pensieri sono i tempi di azione. Nella tua SCORE1 si esegue nuova allocazione di memoria per ogni corsa

valid[i] = new ValidWord 

questo a sua volta permette memoria processo di applicazione trova e poi o inizializzare o creare un nuovo blocco di memoria, impostare i valori e le copie sopra la nuova creato blocco alla posizione originale (non ricordo quale, non il punto però).

Il punto che sto tentando di fare è che ora si richiede all'applicazione di eseguire 14000 operazioni di lettura/scrittura in memoria, ognuna delle quali richiede x quantità di (micro) secondi. E se viene assegnata nuova memoria, è necessario trovare sezioni di memoria di dimensioni corrette.

L'analisi delle prestazioni del codice è un argomento piuttosto ampio, e suppongo che solo i programmatori incorporati lo utilizzino quotidianamente. Ricorda solo che ogni affermazione che fai ha delle operazioni ad essa collegate. Leggendo Vector<bool> e Vector<int> per esempio, il bool avrà un tempo di lettura più lento perché ha bisogno di dividere la memoria in bit e quindi restituire un valore, in cui l'int può recuperare pezzi più grandi di memoria.

Bene, questo è il mio valore di 2 centesimi, spero che ti dia una migliore idea di cosa cercare. Ho un buon libro a casa che spiega come analizzare le linee di codice e i tempi di elaborazione che userà. vedrò se riesco a ottenere una sospensione (recentemente spostato) e aggiornare il nome per te.

1

Qual è l'obiettivo del programma? Score1 e Score2 non ci dicono nulla su ciò che l'algoritmo sta tentando di fare. Sembra che stia cercando di trovare qualsiasi parola che sia composta da tre lettere con tutte le maiuscole. La U rimossa è una parola valida e viene aggiunta alla lista?

Qual è il punto di chiamata Parallel.Per favore su un gruppo di istanze del Programma quando ogni cosa viene passata esattamente nello stesso input? E creare sempre uno StringBuilder per ogni parola non è un buon approccio. Desiderate ridurre al minimo le nuove chiamate in aree critiche per le prestazioni al fine di ridurre il numero di volte in cui il GC deve intervenire.

ho eseguito il programma su file di testo: http://introcs.cs.princeton.edu/data/words.txt

  • Run 1 Totale Media Time = 0,160,149 mila sec
  • Run 2 Tempo complessivo medio = 0,056,846 mila sec

esecuzione sotto il VS 2010 Il profilo di campionamento mostra che Report1 è circa 78 volte più lento di Report2 e rappresenta la maggior parte della differenza. Principalmente a causa di tutte le string.Format e Aggiungi chiamate.

Score1 e Score2 sono più o meno gli stessi in velocità con Score1 andando leggermente più lento a causa del tempo aggiuntivo in StringBuilder.ctor e clr.dll.

Ma ho il sospetto che il tuo algoritmo possa essere riscritto senza che tutti i costruttori di stringhe o le allocazioni siano di un ordine di grandezza più veloce.

+0

Ho rimosso l'input diverso dalle routine di punteggio per semplificare il codice inviato. +1 per avere il tempo di delinearlo. Stavo tentando di creare un profilo prima di postare, ma ho avuto un momento difficile. Il profiler stava mostrando un punto diverso come collo di bottiglia. – jlim