2011-02-09 14 views
12

Estaba jugando con C# y quería acelerar un programa. Hice cambios y pude hacerlo. Sin embargo, necesito ayuda para entender por qué el cambio lo hizo más rápido.Ayuda para comprender la optimización de C#

He intentado reducir el código a algo más fácil de entender en una pregunta. Score1 y Report1 es la forma más lenta. Score2 y Report2 es la forma más rápida. El primer método primero almacena una cadena y un int en una estructura en paralelo. A continuación, en un bucle en serie, recorre una matriz de esas estructuras y escribe sus datos en un búfer. El segundo método primero escribe los datos en un búfer de cadena en paralelo. A continuación, en un bucle en serie, escribe los datos de cadena en un búfer. Aquí están algunas veces muestra de ejecución:

Run 1 Total Promedio de tiempo = 0,492087 seg Run 2 Total Promedio de tiempo = 0,273619 seg

Cuando estaba trabajando con una versión anterior no paralelo a esto, los tiempos eran casi lo mismo. ¿Por qué la diferencia con la versión paralela?

Incluso si reduzco el ciclo en Informe1 para escribir una sola línea de salida en el búfer, aún es más lento (tiempo total aproximadamente .42 segundos).

Este es el código simplificado:.

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

Por qué estás tratando de clasificar tales como Informe1 código diferente y Report2? Report1 contiene un bucle y Report2 no. ¿Tal vez en la versión no paralela el compilador de C# desenrolló el bucle o alguna otra magia? – Earlz

+0

Reducir el bucle de Report1 a una iteración ayuda un poco (.42 seg), pero después de la publicación, creo que es la asignación de matriz en Score1. – jlim

+1

Nota: la lista de palabras es de aproximadamente 14,000 líneas de cadenas. Entonces cada llamada de puntaje asigna 14,000 estructuras. – jlim

Respuesta

1

En primer lugar, está paralelizando las ejecuciones repetidas. Esto mejorará su tiempo de referencia, pero puede que no le ayude mucho en el tiempo real de producción. Para medir con precisión el tiempo que llevará recorrer una lista de palabras, debe tener exactamente una lista de palabras a la vez. De lo contrario, los subprocesos individuales que procesan todas las listas compiten entre sí en cierta medida por los recursos del sistema y el tiempo por lista sufre, incluso si el tiempo para hacer todas las listas en total es más rápido.

Para acelerar el tiempo de procesamiento de una lista de palabras, desea procesar las palabras individuales de la lista en paralelo, para obtener exactamente una lista por vez. Para obtener suficiente definición/tamaño para una buena medición, haga la lista muy larga o procese la lista muchas veces en serie.

En su caso, esto se torna un poco complicado porque el generador de cuerdas necesario para su producto final no está documentado como seguro para subprocesos. Aunque no es tan malo. Aquí está un ejemplo de llamar foreach paralelo para una sola lista de palabras:

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

Sólo tiene que llamar que 20 veces en una forma secuencial normal procesada para bucle. Una vez más, podría terminar los puntos de referencia un poco más lento, pero creo que funcionará en el mundo real un poco más rápido debido a un defecto en su punto de referencia. También tenga en cuenta que escribí esto en la ventana de respuesta —. Nunca he intentado compilarlo, por lo que no es perfecto desde el principio.

Después de fijar el punto de referencia para reflejar con más precisión cómo el código paralelo tendrán impacto en su tiempo de procesamiento en el mundo real, el siguiente paso es hacer una profiling para ver donde el programa está pasando realmente es el momento. Esta es la forma en que sabe qué áreas buscar para mejorar.

Por curiosidad, también me gustaría saber cómo funciona esta versión:

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

Aprecio el código. – jlim

1

El tamaño de una estructura típicamente debe ser inferior a la de un puntero (si el rendimiento es el tema principal Microsoft says que cualquier cosa menos de 16 bytes tiene un mejor rendimiento como una struct si la semántica del tipo de referencia no es necesaria), de lo contrario la sobrecarga para pasarla aumenta (porque pasa por valor) y sería más de lo que hubiera sido para pasar un puntero. Su estructura contiene un puntero y un int (lo que lo convierte en algo más que un puntero) por lo que estaría experimentando una sobrecarga debido a esto.

Ver el Cuándo utilizar las estructuras sección de this article.

+0

* El tamaño de una estructura normalmente debe ser menor que el de un puntero (Microsoft recomienda no más de 16 bytes) * - Una referencia en C# no está cerca de 16 bytes. Es difícil hacer un tipo de estructura útil que sea menor que los bits 32/64 (+ algunos gastos generales de mantenimiento) que requiere una referencia. –

+0

@Ed S: por eso es solo una recomendación y no una aplicación. Sus estructuras podrían ser más grandes, simplemente teniendo en cuenta que vienen con un golpe de rendimiento (que debe sopesar y considerar al decidir sobre una estructura o clase como Microsoft tiene con muchas de sus estructuras). – Cornelius

+2

Ok, pero es una mala recomendación. Quiero decir realmente, por favor muéstrame una estructura útil que sea <= el tamaño de una referencia. Por su recomendación, cada tipo de estructura en el marco falla, por lo que no estoy seguro de cómo podría decirse que sería "típico". –

-1

Sólo una idea: No he hecho ninguna medición, pero, por ejemplo, esta línea:

foreach (char c in words[i]) 
  1. creo que sería mejor al crear una variable temporal para la palabra actual.

  2. Además, el iterador de una cadena puede ser más lento.

El código se convertiría en algo así:

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

El nuevo también podría ser un problema de rendimiento como alguien ya señalado. Como dije en mi comentario, agregar más datos de perfil ayudaría a señalar exactamente qué está sucediendo. O echa un vistazo al código generado.

+6

-1, la versión 'foreach' está optimizada específicamente por el compilador para no calcular' Length' una y otra vez. Muchos de "Creo" y no lo suficiente "Probé" hacen una respuesta pobre. – Blindy

+4

De acuerdo. Las conjeturas sobre qué optimizaciones realizará o no la compilación son completamente inútiles. –

+0

No sé por qué las personas mod tanto ... Si haces algunas búsquedas, verás que el Código IL generado por el foreach y el para es diferente. (http://abi.exdream.com/Blog/post/2009/04/22/For-vs-Foreach-Performance.aspx Incluso Oakcool publicó un enlace interesante para el caso). Además, solo te doy algunas ideas para ayudarte a investigar el asunto. No haré el perfil en tu lugar. – Sauleil

1

Intenté ejecutarlo a través de un generador de perfiles, pero no estoy confiando en los resultados que obtuve. (Run1 lleva menos tiempo que Run2 en ella.) Por lo tanto no hay ninguna respuesta concreta, pero mi sospecha es que la validez array [] es el culpable:

  1. Esa es una potencialmente grande de asignación de memoria que Run1 está haciendo y Run2 no. Asignar grandes trozos de memoria puede llevar mucho tiempo.

  2. Es posible que la matriz esté llegando lejos de cualquier otro dato de trabajo en la memoria física. Por lo menos, es lo suficientemente grande como para terminar en el gran montón de objetos, mientras que parece que casi todo lo demás terminará en la pila o en el pequeño montón de objetos. Eso podría significar que la función Score1 está teniendo que lidiar con más errores de caché que la función Score2.

Podría ser un problema mucho más pequeño en el código de serie, donde solo puede ocurrir una vez en un momento dado. Sin embargo, cuando está ocurriendo para muchos subprocesos simultáneamente, el problema puede complicarse, de modo que lo que originalmente causó una falta o dos de caché adicional está causando fallas en la página.

+0

Tuve algunos problemas para perfilar el programa original también. Estaba señalando una rutina diferente como el cuello de botella. Aprecio la idea de posibles fallas de página. – jlim

1

Así que hay una publicación en el proyecto de código que ayuda a responder eso.

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

No se verá que el código generado es slitely diferente, por lo que en una larga lista, que va a perder unos Ciclos para esas pocas líneas adicionales, y que va a cambiar el tiempo final.

1

Bueno, acabo de navegar sobre su código y mis primeros pensamientos son los tiempos de acción. En su SCORE1 realizar nueva asignación de memoria para cada ejecución

valid[i] = new ValidWord 

esto a su vez permite la memoria del proceso de aplicación encuentra y luego o bien inicializar o crear un nuevo bloque de memoria, la configuración de los valores y las copias sobre el recién bloque creado a la ubicación original (se me olvida cuál, pero no el punto).

Lo que estoy tratando de decir es que ahora necesita que la aplicación realice 14000 operaciones de lectura/escritura de memoria, todo lo cual toma una cantidad x de (micro) segundos. Y si se asigna nueva memoria, necesita encontrar secciones de memoria del tamaño correcto.

El Análisis de rendimiento de código es un tema bastante amplio, y supongo que solo los programadores integrados realmente lo utilizan a diario. Solo recuerda que cada declaración que hagas tiene operaciones vinculadas a ella. Leyendo Vector<bool> y Vector<int> por ejemplo, el bool tendrá un tiempo de lectura más lento porque necesita dividir la memoria en bits y luego devolver un valor, donde el int puede recuperar trozos de memoria más grandes.

Bueno, ese es mi valor de 2 centavos, espero que te dé una mejor idea de qué buscar. Tengo un buen libro en casa sobre cómo analizar las líneas de código y los tiempos de proceso que utilizará. veré si puedo obtener un control (recientemente movido) y actualizar el nombre por usted.

1

¿Cuál es el objetivo del programa? Score1 y Score2 no nos dicen nada sobre lo que el algoritmo está tratando de hacer. Parece que le gusta tratar de encontrar cualquier palabra que tenga tres letras con todas las mayúsculas. ¿Se eliminó una U? ¿Es una palabra válida y se agrega a la lista?

¿Cuál es el punto de llamar a Parallel.Foreach en un montón de instancias de programa cuando se pasa cada cosa exactamente la misma entrada? Y siempre crear un StringBuilder para cada palabra no es un buen enfoque. Desea minimizar cualquier nueva llamada en áreas críticas para el rendimiento a fin de reducir el número de veces que el GC tiene que funcionar.

me encontré con su programa en el archivo de texto: http://introcs.cs.princeton.edu/data/words.txt

  • Run 1 Total Promedio de tiempo = 0,160149 seg
  • Run 2 Total Promedio de tiempo = 0,056846 seg

Correr bajo la VS El generador de perfiles de 2010 muestra que el Informe1 es aproximadamente 78 veces más lento que el Informe2 y representa la mayor parte de la diferencia. Principalmente debido a toda la cadena. Formatear y agregar llamadas.

Score1 y Score2 son aproximadamente los mismos en cuanto a velocidad, con Score1 un poco más lento debido al tiempo adicional en StringBuilder.ctor y clr.dll.

Pero sospecho que su algoritmo podría reescribirse sin que todos los constructores de cadenas o asignaciones sean de un orden de magnitud más rápido.

+0

Quité la entrada que era diferente a las rutinas de puntuación para simplificar el código publicado. +1 por tomarse el tiempo para perfilarlo. Estaba intentando hacer un perfil antes de publicarlo pero tuve un momento difícil. El perfilador mostraba un punto diferente como un cuello de botella. – jlim

Cuestiones relacionadas