Muy a menudo en SO me encuentro comparando pequeños fragmentos de código para ver qué implementación es más rápida.
Muy a menudo veo comentarios de que el código de evaluación comparativa no tiene en cuenta el jitting o el recolector de basura.
Tengo la siguiente función de evaluación comparativa simple que he evolucionado lentamente:
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
Uso:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
¿Tiene esta implementación algún defecto? ¿Es lo suficientemente bueno para mostrar que la implementación X es más rápida que la implementación Y en las iteraciones Z? ¿Puedes pensar en alguna forma de mejorar esto?
EDITAR Está bastante claro que se prefiere un enfoque basado en el tiempo (a diferencia de las iteraciones), ¿alguien tiene alguna implementación en la que las verificaciones de tiempo no afecten el rendimiento?
fuente
Respuestas:
Aquí está la función modificada: según lo recomendado por la comunidad, siéntase libre de modificar esto, es una wiki de la comunidad.
Asegúrese de compilar en Release con las optimizaciones habilitadas y ejecute las pruebas fuera de Visual Studio . Esta última parte es importante porque el JIT limita sus optimizaciones con un depurador adjunto, incluso en el modo de lanzamiento.
fuente
La finalización no se completará necesariamente antes de las
GC.Collect
devoluciones. La finalización se pone en cola y luego se ejecuta en un hilo separado. Este hilo aún podría estar activo durante sus pruebas, afectando los resultados.Si desea asegurarse de que la finalización se haya completado antes de comenzar sus pruebas, es posible que desee llamar
GC.WaitForPendingFinalizers
, que se bloqueará hasta que se borre la cola de finalización:fuente
GC.Collect()
una vez más?Collect
está ahí para asegurarse de que los objetos "finalizados" también se recopilen.Si desea eliminar las interacciones GC de la ecuación, es posible que desee ejecutar su llamada de 'calentamiento' después de la llamada GC.Collect, no antes. De esa manera, sabrá que .NET ya tendrá suficiente memoria asignada del sistema operativo para el conjunto de trabajo de su función.
Tenga en cuenta que está realizando una llamada de método no en línea para cada iteración, así que asegúrese de comparar las cosas que está probando con un cuerpo vacío. También tendrá que aceptar que solo puede cronometrar de manera confiable las cosas que son varias veces más largas que una llamada a un método.
Además, dependiendo del tipo de cosas que esté perfilando, es posible que desee ejecutar su ejecución basada en el tiempo durante una cierta cantidad de tiempo en lugar de una cierta cantidad de iteraciones; puede tender a llevar a números más fácilmente comparables sin tener que tener una ejecución muy corta para la mejor implementación y / o una muy larga para la peor.
fuente
Evitaría pasar al delegado en absoluto:
Un código de ejemplo que conduce al uso del cierre:
Si no está al tanto de los cierres, eche un vistazo a este método en .NET Reflector.
fuente
IDisposable
.Creo que el problema más difícil de superar con métodos de evaluación comparativa como este es tener en cuenta los casos extremos y lo inesperado. Por ejemplo: "¿Cómo funcionan los dos fragmentos de código con una alta carga de CPU / uso de la red / destrucción del disco / etc.?" Son excelentes para verificaciones lógicas básicas para ver si un algoritmo en particular funciona de manera significativa más rápido que otro. Pero para probar correctamente la mayor parte del rendimiento del código, tendría que crear una prueba que mida los cuellos de botella específicos de ese código en particular.
Todavía diría que probar pequeños bloques de código a menudo tiene poco retorno de la inversión y puede alentar el uso de código demasiado complejo en lugar de un código simple de mantenimiento. Escribir un código claro que otros desarrolladores, o yo mismo 6 meses después, podamos entender rápidamente, tendrá más beneficios de rendimiento que un código altamente optimizado.
fuente
Llamaría
func()
varias veces para el calentamiento, no solo una.fuente
Sugerencias para mejorar
Detectar si el entorno de ejecución es bueno para la evaluación comparativa (como detectar si hay un depurador adjunto o si la optimización jit está deshabilitada, lo que daría lugar a mediciones incorrectas).
Medir partes del código de forma independiente (para ver exactamente dónde está el cuello de botella).
Respecto al # 1:
Para detectar si hay un depurador adjunto, lea la propiedad
System.Diagnostics.Debugger.IsAttached
(recuerde también manejar el caso en el que el depurador inicialmente no está adjunto, pero se adjunta después de un tiempo).Para detectar si la optimización de jit está deshabilitada, lea la propiedad
DebuggableAttribute.IsJITOptimizerDisabled
de los ensamblados relevantes:Respecto al # 2:
Esto puede hacerse de muchas maneras. Una forma es permitir que se proporcionen varios delegados y luego medir a esos delegados individualmente.
Respecto al # 3:
Esto también podría hacerse de muchas formas, y diferentes casos de uso exigirían soluciones muy diferentes. Si el punto de referencia se invoca manualmente, entonces escribir en la consola podría estar bien. Sin embargo, si el sistema de compilación realiza la evaluación comparativa automáticamente, es probable que escribir en la consola no sea tan bueno.
Una forma de hacerlo es devolver el resultado de la evaluación comparativa como un objeto fuertemente tipado que se puede consumir fácilmente en diferentes contextos.
Etimo.
Otro enfoque es utilizar un componente existente para realizar los puntos de referencia. De hecho, en mi empresa decidimos lanzar nuestra herramienta de referencia al dominio público. En esencia, administra el recolector de basura, el jitter, los calentamientos, etc., tal como sugieren algunas de las otras respuestas aquí. También tiene las tres características que sugerí anteriormente. Gestiona varios de los temas tratados en el blog de Eric Lippert .
Esta es una salida de ejemplo donde se comparan dos componentes y los resultados se escriben en la consola. En este caso, los dos componentes comparados se denominan 'KeyedCollection' y 'MultiplyIndexedKeyedCollection':
Hay un paquete NuGet , un paquete NuGet de muestra y el código fuente está disponible en GitHub . También hay una publicación en el blog .
Si tiene prisa, le sugiero que obtenga el paquete de muestra y simplemente modifique los delegados de muestra según sea necesario. Si no tiene prisa, puede ser una buena idea leer la publicación del blog para comprender los detalles.
fuente
También debe ejecutar una pasada de "calentamiento" antes de la medición real para excluir el tiempo que el compilador JIT dedica a modificar su código.
fuente
Según el código que esté evaluando y la plataforma en la que se ejecuta, es posible que deba tener en cuenta cómo la alineación del código afecta el rendimiento . Para hacerlo, probablemente se requiera un contenedor externo que ejecute la prueba varias veces (¿en dominios o procesos de aplicación separados?), Algunas de las veces llamando primero a "código de relleno" para forzar su compilación JIT, de modo que el código sea comparado para alinearse de manera diferente. Un resultado de prueba completo daría los tiempos en el mejor y el peor de los casos para las diversas alineaciones de código.
fuente
Si está intentando eliminar el impacto de la recolección de basura del punto de referencia completo, ¿vale la pena configurarlo
GCSettings.LatencyMode
?Si no es así, y desea que el impacto de la basura creada en
func
sea parte del punto de referencia, ¿no debería también forzar la recolección al final de la prueba (dentro del temporizador)?fuente
El problema básico con su pregunta es la suposición de que una sola medición puede responder a todas sus preguntas. Necesita medir varias veces para obtener una imagen efectiva de la situación y especialmente en un lenguaje de recolección de basura como C #.
Otra respuesta ofrece una forma correcta de medir el rendimiento básico.
Sin embargo, esta única medida no tiene en cuenta la recolección de basura. Un perfil adecuado también representa el peor rendimiento de la recolección de basura distribuida en muchas llamadas (este número es algo inútil ya que la VM puede terminar sin recolectar la basura sobrante, pero sigue siendo útil para comparar dos implementaciones diferentes de
func
).Y también se puede querer medir el peor rendimiento de la recolección de basura para un método que solo se llama una vez.
Pero más importante que recomendar cualquier posible medición adicional específica para el perfil es la idea de que se deben medir múltiples estadísticas diferentes y no solo un tipo de estadística.
fuente