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:
- 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". - ¿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é. - ¿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í.
Gracias por su respuesta y por abordar específicamente los puntos importantes. –
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. :) –
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