tiempo - stopwatch method c#
¿Es mi método de medir el tiempo de ejecución defectuoso? (8)
Lo siento, es largo, pero solo estoy explicando mi línea de pensamiento mientras analizo esto. Preguntas al final.
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 tener en cuenta las diferencias por ejecución y también para obtener los tiempos en que se utilizó mejor el caché.
En un intento de medir los tiempos de ejecución de alguien, se me ocurrió this código después de varias revisiones.
Al final, terminé con este código que produjo los resultados que pretendía capturar sin dar números confusos:
// 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 mide los tiempos de ejecución, usualmente estaban en la forma:
// approach 1 pseudocode start timer; loop N times: run testing code (directly or via function); stop timer; report results;
Esto fue bueno en mi mente ya que con los números, tengo el tiempo total de ejecución y puedo calcular fácilmente el tiempo promedio de ejecución y tendría una buena ubicación de caché.
Pero un conjunto de valores que pensé que era importante tener era el tiempo de ejecución mínimo y máximo de iteración. Esto no se pudo calcular utilizando el formulario anterior. Entonces, cuando escribí mi código de prueba, los escribí de esta forma:
// 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 podría encontrar el tiempo mínimo, el máximo y el promedio, los números en los que estaba interesado. Hasta ahora me di cuenta de que esto podría distorsionar los resultados, ya que la memoria caché podría verse afectada ya que el ciclo no era muy estricto. dándome resultados menos que óptimos.
La forma en que escribí el código de prueba (usando LINQ) agregó sobrecargas adicionales que conocía pero que ignoré ya que solo estaba midiendo el código en ejecución, no las sobrecargas. 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 solo estoy midiendo los tiempos que tomó ejecutar la función de prueba. Los gastos generales asociados con LINQ no se incluyen en los tiempos de ejecución. Para reducir la sobrecarga de crear objetos de temporizador dentro del bucle, hice 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 en general, pero causó un problema menor. Agregué el tiempo total de ejecución en el informe agregando los tiempos de cada iteración, pero proporcioné números confusos ya que los tiempos eran cortos y no reflejaban el tiempo real de ejecución (que generalmente era mucho más largo). Necesitaba medir el tiempo de todo el bucle 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 sobrecarga de AFAIK. (Iniciar y detener el temporizador solo consulta el temporizador de alta resolución). Además, cualquier cambio de contexto que ocurra no es importante para mí, ya que es parte de la ejecución normal de todos modos.
En un momento, forcé que el hilo cediera dentro del bucle para asegurarme de que se le da la oportunidad en algún momento en un momento conveniente (si el código de prueba está enlazado a la CPU y no se bloquea en absoluto). No me preocupan demasiado los procesos que se ejecutan, lo que podría empeorar el caché, ya que de todos modos estaría ejecutando estas pruebas solo. Sin embargo, llegué a la conclusión de que para este caso en particular, era innecesario tenerlo. Aunque podría incorporarlo en la versión final final si resulta beneficioso en general. Tal vez como un algoritmo alternativo para ciertos códigos.
Ahora mis preguntas:
- ¿Tomé algunas decisiones correctas? Algunos equivocados?
- ¿Hice suposiciones erróneas acerca de los objetivos en mi proceso de pensamiento?
- ¿Sería el tiempo mínimo o máximo de ejecución realmente una información útil o es una causa perdida?
- Si es así, ¿qué enfoque sería mejor en general? El tiempo que se ejecuta en un bucle (enfoque 1)? ¿O el tiempo ejecutando sólo el código en cuestión (enfoque 2)?
- ¿Estaría bien usar mi enfoque híbrido en general?
- ¿ Debo ceder (por las razones explicadas en el último párrafo) o es más daño a los tiempos de lo necesario?
- ¿Hay una forma más preferida de hacer esto que no mencioné?
Para que quede claro, no estoy buscando un temporizador de uso múltiple, adecuado para cualquier lugar y preciso. Solo quiero saber de un algoritmo que debería usar cuando quiero un temporizador de ejecución rápida y razonablemente preciso para medir el código cuando una biblioteca u otras herramientas de terceros no están disponibles.
Me inclino a escribir todo mi código de prueba en este formulario si no hay objeciones:
// 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 recompensa, lo ideal sería que me respondieran todas las preguntas anteriores. Espero una buena explicación sobre si mis pensamientos que influyeron en el código aquí están bien justificados (y posiblemente pensamientos sobre cómo mejorarlo si no son óptimos) o si estaba equivocado con un punto, explique por qué es incorrecto y / o innecesario y si Aplica, ofrecen una mejor alternativa.
Para resumir las preguntas importantes y mis pensamientos sobre las decisiones tomadas:
- ¿Es bueno tener el tiempo de ejecución de cada iteración individual en general?
Con los tiempos para cada iteración individual, puedo calcular información estadística adicional como los tiempos de ejecución mínimos y máximos, 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 llevó a mi versión "híbrida". - ¿Es bueno tener un pequeño bucle de carreras antes de que comience la sincronización real?
Desde mi respuesta al pensamiento de Sam Saffron en el bucle, esto es para aumentar la probabilidad de que la memoria a la que se accede constantemente se almacene en caché. De esa manera, estoy midiendo los tiempos solo para cuando todo se almacena en caché, en lugar de algunos de los casos donde el acceso a la memoria no se almacena en caché. - ¿Un
Thread.Yield()
forzado dentro del bucle ayudaría o dañaría los tiempos de los casos de prueba vinculados a la CPU?
Si el proceso estuviera vinculado a la CPU, el programador del sistema operativo reduciría la prioridad de esta tarea, lo que podría aumentar los tiempos debido a la falta de tiempo en la CPU. Si no está enlazado a la CPU, omitiría el rendimiento.
Basándome en las respuestas aquí, escribiré mis funciones de prueba utilizando la implementación final sin los tiempos individuales para el caso general. Si quisiera tener otros datos estadísticos, lo reintroduciría de nuevo en la función de prueba y aplicaría las otras cosas mencionadas aquí.
Creo que su primer ejemplo de código parece ser el mejor enfoque.
Su 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 generar una sobrecarga oculta.
El uso de la clase Cronómetro es bueno 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 iterar la prueba un número menor de veces sin tiempo antes de ingresar al ciclo de tiempo para calentar las memorias caché, búferes, conexiones, manijas, sockets, hilos de subprocesos, etc. que la rutina de prueba puede ejercer.
HTH.
Independientemente del mecanismo para sincronizar su función (y las respuestas aquí parecen estar bien), existe un truco muy simple para erradicar la sobrecarga del propio código de evaluación comparativa, es decir, la sobrecarga del bucle, las lecturas del temporizador y la llamada al método:
Simplemente llame a su código de referencia con un Func<T>
vacío primero, es decir
void EmptyFunc<T>() {}
Esto le dará una línea de base de la sobrecarga de tiempo, que esencialmente puede restar de las últimas mediciones de su función de referencia real.
Por "esencialmente" me refiero a que siempre hay espacio para variaciones cuando se sincroniza un código, debido a la recolección de basura y la programación de procesos y subprocesos. Un enfoque pragmático sería, por ejemplo, realizar una evaluación comparativa de 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 dejar que vaya por debajo de 0, lo que no no tiene sentido
Por supuesto, tendrá que reorganizar un poco su código de referencia. Idealmente, querrá usar exactamente el mismo código para evaluar la función vacía y la función real de referencia, por lo que le sugiero que mueva el bucle de temporización a otra función o al menos mantenga los dos bucles completamente iguales. En resumen
- comparar la función vacía
- Calcula la sobrecarga media del resultado.
- comparar la verdadera función de prueba
- restar la sobrecarga promedio de los resultados de las pruebas
- has terminado
Al hacer esto, el mecanismo de sincronización real de repente se vuelve mucho menos importante.
La lógica en el Enfoque 2 se siente "más correcta" para mí, pero solo soy un estudiante de CS.
Me encontré con este enlace que podría encontrar de interés: http://www.yoda.arachsys.com/csharp/benchmark.html
Me inclinaría hacia lo último, pero consideraría si la sobrecarga de iniciar y detener un temporizador podría ser mayor que la del bucle en sí mismo.
Una cosa a considerar, sin embargo, es si el efecto de la falta de memoria caché de la CPU es realmente algo bueno para tratar de contrarrestar.
Aprovechar las memorias caché de la CPU es algo en el que un enfoque puede superar a otro, pero en los casos del mundo real puede haber una pérdida de memoria caché en cada llamada, por lo que esta ventaja se vuelve intrascendente. En este caso, el enfoque que hizo un menor uso de la memoria caché podría convertirse en el que tenga un mejor rendimiento en el mundo real.
Una cola basada en una matriz o en una lista única enlazada sería un ejemplo; los primeros casi siempre tienen un mayor rendimiento cuando las líneas de caché no se rellenan entre las llamadas, pero sufren más operaciones de cambio de tamaño que las últimas. Por lo tanto, estos últimos pueden ganar en casos del mundo real (tanto más cuanto que son más fáciles de escribir sin bloqueo), aunque casi siempre pierdan en las rápidas iteraciones de las pruebas de tiempo.
Por esta razón, también puede valer la pena probar algunas iteraciones con algo para forzar la descarga del caché. No puedo pensar cuál sería la mejor manera de hacerlo en este momento, por lo que podría regresar y agregar a esto si lo hago.
Mi primer pensamiento es que un bucle 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 sobrecarga muy pequeña, tan pequeña que casi no vale la pena preocuparse, incluso si test () solo toma un microsegundo, y (2) timer.Start () and timer .Stop () tiene su propia sobrecarga, que es probable que afecte los resultados más que el bucle for. Dicho esto, eché un vistazo al cronómetro en Reflector y noté que Start () y Stop () son bastante baratos (es probable que las propiedades de Elapsed * sean más caras, teniendo en cuenta las matemáticas involucradas).
Asegúrese de que la propiedad IsHighResolution del Cronómetro sea verdadera. Si es falso, el Cronómetro usa DateTime.UtcNow, que creo que solo se actualiza cada 15-16 ms.
1. ¿Es bueno tener el tiempo de ejecución de cada iteración individual en general?
Generalmente no es necesario medir el tiempo de ejecución de cada iteración individual, pero es útil para averiguar cuánto varía el rendimiento entre diferentes iteraciones. Para 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 registres cada iteración.
Si encuentra que la desviación estándar es grande, es posible que tenga motivos para registrar cada iteración, para explorar por qué el tiempo sigue cambiando.
Algunas personas han escrito pequeños marcos para ayudarlo a realizar pruebas de rendimiento. Por ejemplo, CodeTimers . Si está probando algo que es tan pequeño y simple que importa la sobrecarga de la biblioteca de referencia, considere ejecutar la operación en un bucle for dentro de la lambda a la 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 de bucles. Pero si utiliza el desenrollado de bucle, recuerde que la mayoría de las aplicaciones del mundo real no utilizan el desenrollado manual de bucle, por lo que sus resultados de referencia pueden exagerar el rendimiento del mundo real.
Para mí, escribí una pequeña clase para recopilar min, max, media y desviación estándar, que se podría usar para 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. ¿Es bueno tener un pequeño bucle de carreras antes de que comience la sincronización real?
Las iteraciones que mida dependerán de si le interesa 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 el "inicio". Puede esperar que la primera iteración (ya veces más de una) se ejecute más lentamente. Como ejemplo extremo, mi biblioteca GoInterfaces toma constantemente 140 milisegundos para producir su primera salida, luego hace 9 más en aproximadamente 15 ms.
Dependiendo de lo que mida el punto de referencia, es posible que si ejecuta el punto de referencia justo después de reiniciar, 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 Thread.Yield () forzado dentro del bucle ayudaría o perjudicaría los tiempos 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 ralentizaría su índice de referencia en general, sino que también reduciría las velocidades medidas. Sus resultados serán más "artificiales", sin reflejar lo que obtendría en el mundo real. Quizás un mejor enfoque sea evitar ejecutar otras tareas al mismo tiempo que su punto de referencia.
Según el tiempo de ejecución del código que está probando, es bastante difícil medir las ejecuciones 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 será un problema. Si está cerca de milisegundos, los resultados probablemente serán demasiado. Si, por ejemplo, tiene 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.
Tiendo a estar de acuerdo con @ Sam Saffron en usar un Cronómetro en lugar de uno por iteración. En su ejemplo, realiza 1000000 iteraciones por defecto. No sé cuál es el costo de crear un solo Cronómetro, pero usted está creando 1000000 de ellos. Posiblemente, eso en sí mismo podría afectar los resultados de sus pruebas. Revisé un poco su "implementación final" 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 eso tendría menos efecto general que la asignación de tantos 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ómetro dos veces en cada iteración. El delta entre será la cantidad de tiempo empleado en la iteración. Usando Cronómetro. Frecuencia, podemos convertir los valores delta a milisegundos.
El uso de la marca de tiempo y la frecuencia para calcular el rendimiento no es necesariamente tan claro como el uso directo de una instancia de cronómetro. 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é si mi idea es mejor o peor que la tuya, pero es ligeramente diferente ;-)
También estoy de acuerdo con el bucle de calentamiento. Dependiendo de lo que esté haciendo su prueba, podría haber algunos costos fijos de inicio que no desea afectar a los resultados generales. El bucle de inicio debería eliminar eso.
Probablemente hay un punto en el que mantener cada resultado de tiempo individual es contraproducente debido al costo de almacenamiento necesario para mantener toda la gama de valores (o temporizadores). Para menos memoria, pero más tiempo de procesamiento, simplemente puede sumar los deltas, calculando el mínimo y el máximo a medida que avanza. Eso tiene el potencial de descartar sus resultados, pero si está preocupado principalmente por las estadísticas generadas en base a las mediciones de iteración invidivual, entonces puede hacer los cálculos de mínimo y máximo fuera del control delta del tiempo:
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 aún así hace el trabajo.
Tuve una pregunta similar aquí .
Prefiero el concepto de usar un solo cronómetro, especialmente si usted es un micro benchamrking. Su código no tiene en cuenta el GC, lo que puede afectar el rendimiento.
Creo que forzar una colección de GC es bastante importante antes de ejecutar las pruebas de prueba, además, no estoy seguro de cuál es el punto de una ejecución de calentamiento de 100.