2010-10-22 21 views
16

Lo siento, es largo, pero solo estoy explicando mi tren de pensamiento mientras analizo esto. Preguntas al final.¿Mi método para medir el tiempo de funcionamiento es defectuoso?

Tengo una comprensión de lo que implica medir los tiempos de ejecución del código. Se ejecuta varias veces para obtener un tiempo de ejecución promedio para dar cuenta de las diferencias por ejecución y también para obtener los tiempos cuando se utilizó mejor la memoria caché.

En un intento de medir los tiempos de ejecución para alguien, se me ocurrió el código this después de varias revisiones.

Al final terminé con este código que produjo los resultados que pretende capturar sin dar cifras engañosas:

// implementation C 
static void Test<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    Console.WriteLine(testName); 
    Console.WriteLine("Iterations: {0}", iterations); 
    var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); 
    var timer = System.Diagnostics.Stopwatch.StartNew(); 
    for (int i = 0; i < results.Count; i++) 
    { 
     results[i].Start(); 
     test(); 
     results[i].Stop(); 
    } 
    timer.Stop(); 
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), timer.ElapsedMilliseconds); 
    Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), timer.ElapsedTicks); 
    Console.WriteLine(); 
} 

de todo el código que he visto que las medidas de tiempos de funcionamiento, se eran por lo general en la forma:

 
// approach 1 pseudocode 
start timer; 
loop N times: 
    run testing code (directly or via function); 
stop timer; 
report results; 

Esto era bueno en mi mente ya que con los números, no tengo el tiempo de ejecución total y fácilmente puede calcular el tiempo medio de ejecución y no tendría buena ca localidad che

Pero un conjunto de valores que pensé que era importante tener eran el tiempo mínimo y máximo de ejecución de la iteración. Esto no se pudo calcular utilizando el formulario anterior. Así que cuando escribí mi código de prueba, las escribí en este formulario:

 
// approach 2 pseudocode 
loop N times: 
    start timer; 
    run testing code (directly or via function); 
    stop timer; 
    store results; 
report results; 

Esto es bueno porque entonces podría encontrar el mínimo, el máximo, así como los tiempos medios, los números que estaba interesado en Hasta ahora. se dio cuenta de que esto podría sesgar los resultados, ya que la caché podría verse afectada ya que el ciclo no era muy ajustado y me da resultados menos que óptimos.


La forma en que escribí el código de prueba (utilizando LINQ) añaden gastos adicionales que yo sabía sobre pero ignorados, ya que sólo estaba midiendo el código en ejecución, no los gastos generales. Aquí fue mi primera versión:

// implementation A 
static void Test<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    Console.WriteLine(testName); 
    var results = Enumerable.Repeat(0, iterations).Select(i => 
    { 
     var timer = System.Diagnostics.Stopwatch.StartNew(); 
     test(); 
     timer.Stop(); 
     return timer; 
    }).ToList(); 
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds)); 
    Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks)); 
    Console.WriteLine(); 
} 

aquí pensé que esto estaba bien, ya que sólo estoy midiendo los tiempos que se tardó en ejecutar la función de prueba. Los gastos generales asociados con LINQ no están incluidos en los tiempos de ejecución. Para reducir la sobrecarga de crear objetos de temporizador dentro del ciclo, realicé la modificación.

// implementation B 
static void Test<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    Console.WriteLine(testName); 
    Console.WriteLine("Iterations: {0}", iterations); 
    var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); 
    results.ForEach(t => 
    { 
     t.Start(); 
     test(); 
     t.Stop(); 
    }); 
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), results.Sum(t => t.ElapsedMilliseconds)); 
    Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), results.Sum(t => t.ElapsedTicks)); 
    Console.WriteLine(); 
} 

Esto mejoró los tiempos generales pero causó un problema menor. Agregué el tiempo total de ejecución en el informe agregando las horas de cada iteración pero dando números engañosos ya que los tiempos eran cortos y no reflejaban el tiempo de ejecución real (que generalmente era mucho más). Necesitaba medir el tiempo de todo el ciclo ahora, así que me alejé de LINQ y terminé con el código que tengo ahora en la parte superior. Este híbrido obtiene los tiempos que creo que son importantes con un mínimo de gastos generales AFAIK. (Comenzar y detener el temporizador solo consulta el temporizador de alta resolución) También cualquier cambio de contexto que se produzca no es importante para mí, ya que es parte de la ejecución normal de todos modos.

En un punto, obligué a la hebra a ceder dentro del ciclo para asegurarse de que se le da la oportunidad en algún momento a la hora conveniente (si el código de prueba está vinculado a la CPU y no bloquea en absoluto). No estoy demasiado preocupado por los procesos en ejecución, lo que podría empeorar la caché, ya que de todos modos correría estas pruebas solo.Sin embargo, llegué a la conclusión de que para este caso particular, era innecesario. Aunque podría incorporarlo en LA versión final final si resulta beneficioso en general. Tal vez como un algoritmo alternativo para cierto código.


Ahora mis preguntas:

  • ¿He hecho algunas decisiones correctas? ¿Algunos equivocados?
  • ¿Hice suposiciones erróneas sobre los objetivos en mi proceso de pensamiento?
  • ¿Los tiempos de ejecución mínimos o máximos realmente serían información útil para tener o es una causa perdida?
  • En caso afirmativo, ¿qué enfoque sería mejor en general? El tiempo corriendo en un bucle (aproximación 1)? ¿O el tiempo corriendo solo el código en cuestión (aproximación 2)?
  • ¿Mi enfoque híbrido estaría bien para usar en general?
  • ¿Debo rendir (por las razones explicadas en el último párrafo) o es más perjudicial que el necesario?
  • ¿Hay alguna manera más preferida de hacer esto que yo no mencioné?

Para que quede claro, no estoy buscando un todo-propósito, usar en cualquier lugar, contador de tiempo preciso. Solo quiero saber de un algoritmo que debería usar cuando quiero un temporizador rápido y razonablemente preciso para medir el código cuando una biblioteca u otras herramientas de terceros no están disponibles.

estoy inclinado a escribir todo mi código de prueba en esta forma debe haber ninguna objeción:

// final implementation 
static void Test<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    // print header 
    var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); 
    for (int i = 0; i < 100; i++) // warm up the cache 
    { 
     test(); 
    } 
    var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process 
    for (int i = 0; i < results.Count; i++) 
    { 
     results[i].Start(); // time individual process 
     test(); 
     results[i].Stop(); 
    } 
    timer.Stop(); 
    // report results 
} 

Para la generosidad, que lo ideal sería tener todas estas preguntas contestadas. Espero una buena explicación sobre si mis pensamientos que influyeron en el código aquí están bien justificados (y posiblemente sobre cómo mejorarlo si no son óptimos) o si estaba equivocado con un punto, explicar por qué es incorrecto y/o innecesario y si aplicable, ofrece una mejor alternativa.

Para resumir las preguntas importantes y mis pensamientos para las decisiones que se toman:

  1. es conseguir que el tiempo de funcionamiento de cada iteración individuo generalmente una buena cosa a tener?
    Con los tiempos para cada iteración individual, puedo calcular información estadística adicional como los tiempos de ejecución mínimo y máximo, así como la desviación estándar. Así que puedo ver si hay factores como el almacenamiento en caché u otras incógnitas que pueden estar sesgando los resultados. Esto condujo a mi versión "híbrida".
  2. ¿Se está ejecutando un pequeño ciclo de ejecuciones antes de que el tiempo real comience también?
    De mi respuesta a Sam Saffron's pensado en el ciclo, esto es para aumentar la probabilidad de que la memoria a la que se accede constantemente se almacenará en caché. De esta forma estoy midiendo los tiempos solo para cuando todo está almacenado en caché, en lugar de algunos de los casos en que el acceso a la memoria no está en la memoria caché.
  3. ¿Forzaría Thread.Yield() forzado dentro del lazo a ayudar o dañar las temporizaciones de los casos de prueba vinculados a la CPU?
    Si el proceso estaba vinculado a la CPU, el planificador del sistema operativo reduciría la prioridad de esta tarea aumentando potencialmente debido a la falta de tiempo en la CPU. Si no está vinculado a la CPU, omitiría el rendimiento.

Sobre la base de las respuestas aquí, voy a estar escribiendo mis funciones de prueba utilizando la aplicación final sin los tiempos individuales para el caso general. Si quisiera tener otros datos estadísticos, los reintroduciría nuevamente en la función de prueba, así como aplicaré las otras cosas mencionadas aquí.

Respuesta

8

Lo primero que pensé es que un circuito tan simple como

for (int i = 0; i < x; i++) 
{ 
    timer.Start(); 
    test(); 
    timer.Stop(); 
} 

es un poco tonto en comparación con:

timer.Start(); 
for (int i = 0; i < x; i++) 
    test(); 
timer.Stop(); 

la razón es que (1) este tipo de bucle "for" tiene una muy pequeña, tan pequeña que casi no vale la pena preocuparse aunque test() solo tome un microsegundo, y (2) timer.Start() y timer.Stop() tengan su propia sobrecarga, lo que es probable que afecte los resultados más que el bucle for. Dicho esto, eché un vistazo a Cronómetro en Reflector y noté que Start() y Stop() son bastante baratos (llamar a las propiedades de Elapsed * es probablemente más caro, considerando los cálculos involucrados.)

Asegúrate de que la propiedad IsHighResolution Cronómetro es cierto. Si es falso, Cronómetro usa DateTime.UtcNow, que creo que solo se actualiza cada 15-16 ms.

1. ¿Es bueno tener en general el tiempo de ejecución de cada iteración individual?

No suele ser necesario medir el tiempo de ejecución de cada iteración individual, pero es útil para averiguar hasta qué punto el rendimiento varía entre las diferentes iteraciones. Con este fin, puede calcular el mínimo/máximo (o k valores atípicos) y la desviación estándar. Solo la estadística "mediana" requiere que registre cada iteración.

Si encuentra que la desviación estándar es grande, puede que tenga razones para razonar para registrar cada iteración, con el fin de explorar por qué el tiempo cambia constantemente.

Algunas personas han escrito pequeños marcos para ayudarlo a realizar pruebas de rendimiento. Por ejemplo, CodeTimers. Si está probando algo tan pequeño y simple que la sobrecarga de la biblioteca de referencia importa, considere ejecutar la operación en un bucle forzado dentro de la lambda que llama la biblioteca de referencia. Si la operación es tan pequeña que importa la sobrecarga de un bucle for (por ejemplo, medir la velocidad de multiplicación), utilice el desenrollado manual del bucle. Pero si utiliza el desenrollado de bucles, recuerde que la mayoría de las aplicaciones del mundo real no utilizan el bucle manual, por lo que sus resultados de referencia pueden exagerar el rendimiento en el mundo real.

Por mi parte he escrito un poco de clase para reunir mínimo, máximo, media y desviación estándar, que pueden utilizarse para aplicar puntos de referencia u otras estadísticas:

// A lightweight class to help you compute the minimum, maximum, average 
// and standard deviation of a set of values. Call Clear(), then Add(each 
// value); you can compute the average and standard deviation at any time by 
// calling Avg() and StdDeviation(). 
class Statistic 
{ 
    public double Min; 
    public double Max; 
    public double Count; 
    public double SumTotal; 
    public double SumOfSquares; 

    public void Clear() 
    { 
     SumOfSquares = Min = Max = Count = SumTotal = 0; 
    } 
    public void Add(double nextValue) 
    { 
     Debug.Assert(!double.IsNaN(nextValue)); 
     if (Count > 0) 
     { 
      if (Min > nextValue) 
       Min = nextValue; 
      if (Max < nextValue) 
       Max = nextValue; 
      SumTotal += nextValue; 
      SumOfSquares += nextValue * nextValue; 
      Count++; 
     } 
     else 
     { 
      Min = Max = SumTotal = nextValue; 
      SumOfSquares = nextValue * nextValue; 
      Count = 1; 
     } 
    } 
    public double Avg() 
    { 
     return SumTotal/Count; 
    } 
    public double Variance() 
    { 
     return (SumOfSquares * Count - SumTotal * SumTotal)/(Count * (Count - 1)); 
    } 
    public double StdDeviation() 
    { 
     return Math.Sqrt(Variance()); 
    } 
    public Statistic Clone() 
    { 
     return (Statistic)MemberwiseClone(); 
    } 
}; 

2. ¿Está teniendo un lazo pequeño de carreras antes de que el tiempo real comience bien también?

Las iteraciones que mida dependen de si le preocupa más el tiempo de inicio, el tiempo de estado estable o el tiempo de ejecución total. En general, puede ser útil registrar una o más ejecuciones por separado cuando se ejecuta "inicio". Puede esperar que la primera iteración (y algunas veces más de una) se ejecute más lentamente. Como ejemplo extremo, mi biblioteca GoInterfaces tarda consistentemente unos 140 milisegundos en producir su primera salida, luego hace 9 más en aproximadamente 15 ms.

Dependiendo de las medidas de referencia, puede encontrar que si ejecuta el punto de referencia inmediatamente después del reinicio, la primera iteración (o las primeras iteraciones) se ejecutará muy lentamente. Luego, si ejecuta el punto de referencia por segunda vez, la primera iteración será más rápida.

3. ¿Un forzado Thread.Yield() dentro del bucle ayuda o daña las temporizaciones de los casos de prueba vinculados a la CPU?

No estoy seguro. Puede borrar los cachés del procesador (L1, L2, TLB), lo que no solo desaceleraría su índice de referencia en general, sino que también reduciría las velocidades medidas. Tus resultados serán más "artificiales", sin reflejar lo que obtendrías en el mundo real. Tal vez un mejor enfoque es evitar ejecutar otras tareas al mismo tiempo que su punto de referencia.

+0

Gracias por su respuesta y por abordar específicamente los puntos importantes. –

+0

p.s., El tiempo de las iteraciones individuales del ciclo no fue para no cronometrar la sobrecarga del ciclo, sino por razones estadísticas. Estoy de acuerdo, eso sería una tontería. :) –

+0

Bueno, estoy un poco sorprendido y agradecido de obtener el impulso de la reputación. No soy realmente un experto en análisis de rendimiento, solo lo hago a veces. Espero que hayas aprendido lo que quisieras aprender :). Sí, siento haber malinterpretado tu código, ya que estabas usando un temporizador diferente en cada iteración. – Qwertie

0

La lógica en Approach 2 me parece "más adecuada", pero solo soy un estudiante de CS.

me encontré con este enlace que le puede resultar de interés: http://www.yoda.arachsys.com/csharp/benchmark.html

+0

Siempre lo he pensado así, pero siempre lo veo en forma de acercamiento 1 por otros desarrolladores. Entonces, o es un mejor enfoque o no pensaron en intentar acercarse a 2 (o 3). Mis observaciones sobre los números que intentan estos diferentes enfoques no me dejan claro. Al menos sé que no estoy solo en esto. :) Y gracias por el enlace. No es realmente el tipo de cosa que estoy buscando pero definitivamente es apreciada. Y me recordó la palabra que pretendía usar para una etiqueta: [punto de referencia]. ;) –

0

Dependiendo de lo que el tiempo de ejecución del código que está probando es, es muy difícil medir las carreras individuales. Si el tiempo de ejecución del código de su prueba es de varios segundos, su enfoque de cronometrar la ejecución específica probablemente no sea un problema. Si está en la vecindad de milisegundos, sus resultados probablemente serán demasiado. Si, por ejemplo, tener un cambio de contexto o una lectura del archivo de intercambio en el momento incorrecto, el tiempo de ejecución de esa ejecución será desproporcionado al tiempo de ejecución promedio.

+0

¿Cuál supone que es un buen umbral para el tiempo mínimo de ejecución del código de prueba? Sé que el código de prueba debe ejecutarse al menos unos pocos milisegundos consistentemente como mínimo. Siempre he tenido como objetivo al menos 10. –

2

Creo que su primera muestra de código parece ser el mejor enfoque.

El primer ejemplo de código es pequeño, limpio y simple, y no utiliza ninguna abstracción importante durante el ciclo de prueba, lo que puede introducir una sobrecarga oculta.

El uso de la clase Cronómetro es una buena cosa ya que simplifica el código que normalmente se tiene que escribir para obtener tiempos de alta resolución.

Una cosa que podría considerar es proporcionar la opción de repetir la prueba un menor número de veces sin tiempo antes de ingresar el ciclo de tiempo para calentar cualquier caché, búfer, conexiones, identificadores, sockets, subprocesos, etc. que la prueba la rutina puede hacer ejercicio.

HTH.

+0

Disculpa, debería haber etiquetado las piezas de código por si acaso alguien se refiriera a él. ¿Se refería a la implementación C (la primera muestra de código enumerada en la pregunta) o la implementación A (el primer intento de código)? –

+0

He visto el pequeño ciclo sin tiempo una vez y pensé que era una buena idea. Creo que lo haré también a partir de ahora. –

0

Tuve una similar question here.

Prefiero mucho el concepto de utilizar un solo cronómetro, especialmente si está realizando microcharking. Su código no es responsable del GC, que puede afectar el rendimiento.

Creo que forzar una recolección de GC es muy importante antes de ejecutar las pruebas, tampoco estoy seguro de qué se trata de ejecutar 100 ciclos de calentamiento.

+0

Al principio, una recolección de basura parece una buena idea. Asegura que haya tanta memoria disponible para las llamadas como sea posible. Puedo hacer eso en el futuro y no veo ninguna razón para no hacerlo. –

+0

La motivación detrás del calentamiento breve antes de la sincronización fue aumentar la probabilidad de que la memoria a la que se accede constantemente esté en la memoria caché para cuando comience el tiempo. De esta forma, no está cronometrando la latencia para acceder a la memoria demasiado, sino el "todo está en caché" óptimo. Sé que esto es muy importante cuando se escribe código de bajo nivel y alto rendimiento, especialmente para código multi-core/multi-threaded. Ahora me considero siempre escribiendo para multi-core.Es decir, si alguien que sabe mejor sabe que esto no es necesario, podría convencerme de lo contrario, o al menos proporcionar una mejor explicación. –

1

Tiendo a estar de acuerdo con @Sam Saffron sobre el uso de un cronómetro en lugar de uno por iteración. En su ejemplo, usted realiza 1000000 iteraciones por defecto. No sé cuál es el costo de crear un solo Cronómetro, pero estás creando 1000000 de ellos. Posiblemente, eso en sí mismo podría afectar los resultados de su prueba. Rehice tu "implementación final" un poco para permitir la medición de cada iteración sin crear 1000000 Cronómetros. Por supuesto, ya que estoy guardando el resultado de cada iteración, estoy asignando 1000000 largos, pero a primera vista parece que tendría menos efecto general que la asignación de muchos Cronómetros.No he comparado mi versión con su versión para ver si la mía arrojaría resultados diferentes.

static void Test2<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    long [] results = new long [iterations]; 

    // print header 
    for (int i = 0; i < 100; i++) // warm up the cache 
    { 
    test(); 
    } 

    var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process 

    long start; 

    for (int i = 0; i < results.Length; i++) 
    { 
    start = Stopwatch.GetTimestamp(); 
    test(); 
    results[i] = Stopwatch.GetTimestamp() - start; 
    } 

    timer.Stop(); 

    double ticksPerMillisecond = Stopwatch.Frequency/1000.0; 

    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t/ticksPerMillisecond), results.Average(t => t/ticksPerMillisecond), results.Max(t => t/ticksPerMillisecond), results.Sum(t => t/ticksPerMillisecond)); 
    Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(), results.Average(), results.Max(), results.Sum()); 

    Console.WriteLine(); 
} 

Estoy usando el método GetTimestamp estático del cronógrafo dos veces en cada iteración. El delta entre será la cantidad de tiempo invertido en la iteración. Utilizando Stopwatch.Frequency, podemos convertir los valores delta a milisegundos.

Usar la marca de tiempo y la frecuencia para calcular el rendimiento no es necesariamente tan claro como usar simplemente una instancia de cronómetro directamente. Pero, usar un cronómetro diferente para cada iteración probablemente no sea tan claro como usar un solo cronómetro para medir todo.

no sé que mi idea es mejor o peor que la suya, pero es un poco diferente ;-)

También ponen de acuerdo sobre el circuito de calentamiento. Dependiendo de lo que haga su prueba, puede haber algunos costos de inicio fijos que no desea afectar los resultados generales. El ciclo de inicio debería eliminar eso.

Probablemente hay un punto en el que mantener cada resultado de temporización individual es contraproducente debido al costo de almacenamiento necesario para mantener todo el conjunto de valores (o temporizadores). Para obtener menos memoria, pero más tiempo de procesamiento, puede simplemente sumar los deltas, calculando el mínimo y el máximo a medida que avanza. Esto tiene el potencial de arrojar fuera de sus resultados, pero si se ocupan principalmente de las estadísticas generadas en base a las mediciones de iteración invidivual, a continuación, sólo puede hacer el mínimo y el cálculo máximo fuera del horario delta time:

static void Test2<T>(string testName, Func<T> test, int iterations = 1000000) 
{ 
    //long [] results = new long [iterations]; 
    long min = long.MaxValue; 
    long max = long.MinValue; 

    // print header 
    for (int i = 0; i < 100; i++) // warm up the cache 
    { 
    test(); 
    } 

    var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process 

    long start; 
    long delta; 
    long sum = 0; 

    for (int i = 0; i < iterations; i++) 
    { 
    start = Stopwatch.GetTimestamp(); 
    test(); 
    delta = Stopwatch.GetTimestamp() - start; 
    if (delta < min) min = delta; 
    if (delta > max) max = delta; 
    sum += delta; 
    } 

    timer.Stop(); 

    double ticksPerMillisecond = Stopwatch.Frequency/1000.0; 

    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", min/ticksPerMillisecond, sum/ticksPerMillisecond/iterations, max/ticksPerMillisecond, sum); 
    Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", min, sum/iterations, max, sum); 

    Console.WriteLine(); 
} 

Parece bastante vieja escuela sin las operaciones de Linq, pero todavía hace el trabajo.

+0

Ah, no estaba seguro de a qué se refería cuando mencionó un solo cronómetro. Tu ejemplo me lo dejó más claro. La cantidad de memoria utilizada no es un gran problema para mí. Pero se me escapó por completo la idea de obtener una marca de tiempo que sería más fácil que mantener cronómetros. –

0

Me inclinaría hacia el último, pero consideraría si la sobrecarga de iniciar y detener un temporizador podría ser mayor que la de un bucle.

Una cosa a considerar, sin embargo, es si el efecto de falta de caché de la CPU es realmente una cosa justa para tratar de contrarrestar?

Aprovechar cachés de CPU es algo en que un enfoque puede ganarle a otro, pero en casos del mundo real puede haber una falta de caché con cada llamada, por lo que esta ventaja se vuelve intrascendente. En este caso, el enfoque que hizo un uso menos apropiado de la memoria caché podría convertirse en el que tenga un mejor rendimiento en el mundo real.

Una cola basada en arreglos o basada en listas únicas sería un ejemplo; el primero casi siempre tiene un mayor rendimiento cuando las líneas de caché no se vuelven a llenar entre llamadas, sino que sufren más las operaciones de cambio de tamaño que las segundas. Por lo tanto, este último puede ganar en casos del mundo real (sobre todo porque son más fáciles de escribir en una forma libre de bloqueo) a pesar de que casi siempre perderán en las iteraciones rápidas de las pruebas de tiempo.

Por esta razón, también puede valer la pena intentar algunas iteraciones con algo para forzar la caché. No puedo pensar cuál sería la mejor manera de hacerlo en este momento, así que podría volver y agregar a esto si lo hago.

+0

El 'cronómetro' simplemente consulta al HRT si está disponible (en mi caso, sí) tanto para iniciar como para detener. Así que hay un mínimo de gastos generales allí. Para el caso general, asumiré el almacenamiento en caché óptimo. Estoy interesado en el algoritmo, no en lo rápido que es mi memoria. Aunque lo tendré en cuenta si me interesan los horarios de inicio. –

+0

Bueno, el algoritmo depende de las diferentes velocidades de memoria (en sí mismos todos los algoritmos son solo matemáticos, y 1 + 1 no toma ningún tiempo para igualar 2). Suponiendo que el almacenamiento en caché óptimo establece los sesgos con respecto a la memoria en una dirección particular, suponiendo que el mal almacenamiento en caché lo establece en otro, ambos sesgos dependen de las velocidades de memoria relativas. –

4

Independientemente del mecanismo de temporización de su función (y las respuestas aquí parecen correctas) hay un truco muy simple para erradicar la sobrecarga del código de referencia, es decir, la sobrecarga del ciclo, lecturas de temporizador y método -call:

Simplemente llame a su código de evaluación comparativa con un vacío Func<T> primera, es decir

void EmptyFunc<T>() {} 

Esto le dará una línea de base de la temporización sobrecarga, lo que puede restar esencialmente de las últimas medidas de su real función de referencia.

Por "esencialmente" me refiero a que siempre hay lugar para variaciones cuando se sincroniza un código, debido a la recolección de basura y la programación de subprocesos y procesos. Un enfoque pragmático sería, por ejemplo, sea ​​para comparar la función vacía, encontrar la sobrecarga promedio (tiempo total dividido por iteraciones) y luego restar ese número de cada resultado de tiempo de la función comparativa real, pero no lo deje por debajo de 0, lo cual no tendría sentido.

Por supuesto, tendrá que volver a organizar su código de evaluación comparativa. Lo ideal es utilizar el mismo código mismo código para comparar la función vacía y la función comparativa real, por lo que sugiero que mueva el ciclo de tiempo en otra función o al menos mantenga los dos bucles por completo por igual. En resumen

  1. referencia a la función vacío
  2. calcular la sobrecarga promedio del resultado
  3. referencia la verdadera prueba de la función
  4. restar los gastos generales promedio de los resultados de los pruebas
  5. haya terminado

Al hacerlo, el mecanismo de sincronización real de repente se vuelve mucho menos importante.

+0

No estoy demasiado preocupado por la sobrecarga del bucle o las llamadas de función. Pero esa es una excelente manera de cancelarlo. –

Cuestiones relacionadas