java profiler

java - ¿Cómo escribir un perfilador?



visualvm profiler (5)

Mucho alentador, ¿no?

Los perfiladores no son demasiado duros si solo intenta hacerse una idea razonable de dónde gasta el programa la mayor parte de su tiempo. Si te molesta la alta precisión y la interrupción mínima, las cosas se ponen difíciles.

Entonces, si usted solo quiere las respuestas que le daría un generador de perfiles, busque una que otro haya escrito. Si estás buscando el desafío intelectual, ¿por qué no probar uno?

He escrito un par, para entornos de tiempo de ejecución que los años han vuelto irrelevantes.

Hay dos enfoques

  • agregando algo a cada función u otro punto significativo que registra la hora y dónde está.

  • tener un temporizador que suena regularmente y echar un vistazo donde está el programa actualmente.

La versión de JVMPI parece ser la primera, el enlace proporcionado por uzhin muestra que puede informar sobre un buen número de cosas (ver sección 1.3). Lo que se ejecuta se modifica para hacer esto, por lo que la creación de perfiles puede afectar el rendimiento (y si está perfilando lo que de otra manera sería una función muy liviana, pero a menudo llamada, puede inducir a error).

Si puede obtener un temporizador / interrupción que le indique dónde estaba el contador del programa en el momento de la interrupción, puede usar la tabla de símbolos / información de depuración para determinar en qué función se encontraba en ese momento. Esto proporciona menos información pero puede ser menos perjudicial. Se puede obtener un poco más de información al recorrer la pila de llamadas para identificar a las personas que llaman, etc. No tengo idea de si esto es posible incluso en Java ...

Pablo.

¿Me gustaría saber cómo escribir un generador de perfiles? ¿Qué libros y / o artículos recomendaron? ¿Alguien puede ayudarme por favor?

Alguien ya ha hecho algo como esto?




Como otra respuesta, simplemente miré a LukeStackwalker en sourceforge. Es un bonito y pequeño ejemplo de una pila de muestras, y un buen lugar para comenzar si quieres escribir un generador de perfiles.

Aquí, en mi opinión, es lo que hace bien:

  • Muestrea la pila de llamadas completa.

Suspiro ... tan cerca pero tan lejos. Aquí, IMO, es lo que debería hacer (y otros samplers de stack como xPerf):

  • Debería conservar las muestras de pila sin procesar. Tal como está, se resume en el nivel de función a medida que muestrea. Esto pierde la información clave del número de línea que ubica los sitios problemáticos de llamadas.

  • No es necesario tomar tantas muestras, si el almacenamiento para contenerlas es un problema. Dado que los problemas de rendimiento típicos cuestan de 10% a 90%, 20-40 muestras los mostrarán con bastante fiabilidad. Cientos de muestras proporcionan más precisión de medición, pero no aumentan la probabilidad de localizar los problemas.

  • La IU debe resumirse en términos de enunciados, no de funciones. Esto es fácil de hacer si se guardan las muestras en bruto. La medida clave para adjuntar a una declaración es la fracción de muestras que lo contiene. Por ejemplo:

    5/20 MyFile.cpp: 326 para (i = 0; i <strlen (s); ++ i)

Esto dice que la línea 326 en MyFile.cpp apareció en 5 de 20 muestras, en el proceso de llamar a strlen . Esto es muy significativo, ya que puede ver el problema al instante y sabe cuánto tiempo puede esperar para solucionarlo. Si reemplaza strlen(s) por s[i] , ya no pasará más tiempo en esa llamada, por lo que estas muestras no ocurrirán, y la aceleración será aproximadamente 1 / (1-5 / 20) = 20 / ( 20-5) = 4/3 = aceleración del 33%. (Gracias a David Thornley por este código de muestra).

  • La IU debe tener una vista "mariposa" que muestre las declaraciones. (Si muestra funciones también, está bien, pero las declaraciones son lo que realmente importa). Por ejemplo:

    3/20 MyFile.cpp: 502 MyFunction (myArgs)
    2/20 HisFile.cpp: 113 MyFunction (hisArgs)

    5/20 MyFile.cpp: 326 para (i = 0; i <strlen (s); ++ i)

    5/20 strlen.asm: 23 ... algún código de ensamblado ...

En este ejemplo, la línea que contiene la declaración for es el "foco de atención". Ocurrió en 5 muestras. Las dos líneas de arriba dicen que en 3 de esas muestras, se llamó desde MyFile.cpp:502 , y en 2 de esas muestras, se llamó desde HisFile.cpp:113 . La línea debajo dice que en todas las 5 muestras, estaba en strlen (no es sorpresa). En general, la línea de enfoque tendrá un árbol de "padres" y un árbol de "hijos". Si por alguna razón, la línea de enfoque no es algo que pueda arreglar, puede subir o bajar. El objetivo es encontrar líneas que pueda corregir que estén en tantas muestras como sea posible.

IMPORTANTE: la creación de perfiles no se debe considerar como algo que se hace una vez . Por ejemplo, en la muestra anterior, obtuvimos una aceleración de 4/3 al arreglar una línea de código. Cuando se repite el proceso, otras líneas de código problemáticas deberían mostrarse a 4/3 la frecuencia que tenían antes, y así ser más fáciles de encontrar. Nunca he oído hablar de personas que hablen de iterar en el proceso de creación de perfiles, pero es crucial para obtener grandes aceleraciones compuestas en general.

PD: Si una declaración ocurre más de una vez en una sola muestra, eso significa que está teniendo lugar la recursión. No es un problema. Todavía solo cuenta como una muestra que contiene la declaración. Todavía es el caso que el costo de la declaración es aproximado por la fracción de muestras que lo contiene.


Una vez escribí una, principalmente como un intento de hacer que el "muestreo profundo" sea más fácil de usar. Cuando haces el método manualmente, se explica aquí . Se basa en el muestreo, pero en lugar de tomar una gran cantidad de muestras pequeñas, toma una pequeña cantidad de muestras grandes.

Puede decirle, por ejemplo, que la instrucción I (generalmente una llamada a función) le está costando un porcentaje X del tiempo total de ejecución, más o menos, ya que aparece en la pila en X% de muestras.

Piénselo, porque este es un punto clave . La pila de llamadas existe mientras el programa se esté ejecutando. Si una instrucción de llamada particular I está en la pila X% del tiempo, entonces si esa instrucción pudiera desaparecer, ese X% de tiempo desaparecería. Esto no depende de la cantidad de veces I se ejecute o de cuánto tiempo tome la llamada a la función. Entonces los temporizadores y los contadores están perdiendo el punto. Y, en cierto sentido, todas las instrucciones son instrucciones de llamada, incluso si solo llaman a microcódigo.

La muestra se basa en la premisa de que es mejor conocer la dirección de la instrucción I con precisión ( porque eso es lo que está buscando ) que saber el número X% con precisión. Si sabe que podría ahorrar aproximadamente el 30% de tiempo recodificando algo, ¿realmente le preocupa que pueda estar apagado en un 5%? Aún querrás arreglarlo. La cantidad de tiempo que realmente ahorra no será menor o menor si conoce X con precisión.

Por lo tanto, es posible extraer muestras de un temporizador, pero, francamente, me pareció igual de útil activar una interrupción cuando el usuario presiona ambas teclas de cambio al mismo tiempo. Dado que 20 muestras generalmente son suficientes, y de esta manera puede estar seguro de tomar muestras en un momento relevante (es decir, no mientras esperaba la entrada del usuario) fue bastante adecuado. Otra forma sería hacer solo las muestras accionadas por temporizador mientras el usuario mantiene presionadas ambas teclas de mayúsculas (o algo así).

No me preocupaba que la toma de muestras pudiera ralentizar el programa, porque el objetivo no era medir la velocidad, sino localizar las instrucciones más costosas. Después de arreglar algo, la aceleración general es fácil de medir.

Lo principal que el generador de perfiles proporcionó fue una interfaz de usuario para que pueda examinar los resultados sin dolor. Lo que sale de la fase de muestreo es una colección de muestras de la pila de llamadas, donde cada muestra es una lista de direcciones de instrucciones, donde cada instrucción, excepto la última, es una instrucción de llamada. La interfaz de usuario era principalmente lo que se llama una "vista de mariposa". Tiene un "foco" actual, que es una instrucción particular. A la izquierda se muestran las instrucciones de llamada inmediatamente encima de esa instrucción, como se seleccionó de las muestras de la pila. Si la instrucción de enfoque es una instrucción de llamada, las instrucciones a continuación aparecen a la derecha, como se seleccionó de las muestras. En la instrucción de enfoque se muestra un porcentaje, que es el porcentaje de pilas que contiene esa instrucción. De manera similar, para cada instrucción a la izquierda o derecha, el porcentaje se desglosa por la frecuencia de cada instrucción. Por supuesto, la instrucción se representó por archivo, número de línea y el nombre de la función en la que se encontraba. El usuario puede explorar fácilmente los datos haciendo clic en cualquiera de las instrucciones para convertirlo en el nuevo foco.

Una variación de esta interfaz de usuario trata a la mariposa como bipartita, que consiste en capas alternas de instrucciones de llamada de función y las funciones que las contienen. Eso puede dar un poco más de claridad de tiempo en cada función.

Tal vez no sea obvio, por lo que vale la pena mencionar algunas propiedades de esta técnica.

  • La recursividad no es un problema, porque si una instrucción aparece más de una vez en una muestra de pila determinada, eso todavía cuenta como una sola muestra que la contiene. Sigue siendo cierto que el tiempo estimado que se ahorraría al eliminarlo es el porcentaje de pilas en las que se encuentra.

  • Tenga en cuenta que esto no es lo mismo que un árbol de llamadas. Le da el costo de una instrucción sin importar cuántas ramas diferentes de un árbol de llamadas se encuentre.

  • El rendimiento de la IU no es un problema, ya que el número de muestras no necesita ser muy grande. Si una instrucción en particular I es el enfoque, es bastante simple encontrar cómo las muestras pueden contenerlo, y para cada instrucción adyacente, cuántas de las muestras que contienen I también contienen la instrucción adyacente al lado.

  • Como se mencionó anteriormente, la velocidad de muestreo no es un problema, porque no estamos midiendo el rendimiento, estamos diagnosticando. El muestreo no sesga los resultados, porque el muestreo no afecta lo que hace el programa en general. Un algoritmo que toma N instrucciones para completar todavía toma N instrucciones, incluso si se detiene varias veces.

  • A menudo me preguntan cómo probar un programa que se completa en milisegundos. La respuesta simple es envolverlo en un bucle externo para que tome suficiente tiempo para muestrear. Puede averiguar qué tarda X% de tiempo, eliminarlo, obtener la aceleración X% y luego eliminar el bucle externo.

Este pequeño generador de perfiles, al que llamé YAPA (otro analizador de rendimiento) estaba basado en DOS e hice una pequeña demostración, pero cuando tenía que trabajar mucho, recurría al método manual. La razón principal de esto es que la pila de llamadas por sí sola a menudo no es suficiente información de estado para decirle por qué se está gastando un ciclo en particular. Es posible que también necesite conocer otra información del estado para tener una idea más completa de lo que el programa estaba haciendo en ese momento. Como el método manual me pareció bastante satisfactorio, archivé la herramienta.

Un punto que a menudo se pierde cuando se habla sobre el perfil es que puede hacerlo varias veces para encontrar problemas múltiples. Por ejemplo, supongamos que la instrucción I1 está en la pila el 5% del tiempo e I2 está en la pila el 50% del tiempo. Veinte muestras encontrarán fácilmente I2 , pero quizás no I1 . Entonces arreglas I2 . Luego lo haces todo de nuevo, pero ahora I1 toma el 10% del tiempo, por lo que probablemente lo vean 20 muestras. Este efecto de ampliación permite que las aplicaciones repetidas de perfiles logren grandes factores de aceleración compuestos.