Capturando el tiempo de salida de la función con __gnu_mcount_nc
Estoy intentando crear algunos perfiles de rendimiento en una plataforma integrada prototipo con poco soporte.
Observo que el indicador -pg de GCC hace que __gnu_mcount_nc
se inserten procesadores al ingresar a cada función. No hay ninguna implementación __gnu_mcount_nc
disponible (y el proveedor no está interesado en ayudar); sin embargo, como es trivial escribir una que simplemente registre el marco de la pila y el recuento del ciclo actual, lo he hecho; esto funciona bien y produce resultados útiles en términos de gráficos de llamante/destinatario y funciones llamadas con mayor frecuencia.
Realmente me gustaría obtener información sobre el tiempo dedicado a los cuerpos de las funciones también, sin embargo, tengo dificultades para entender cómo abordar esto con solo la entrada, pero no la salida, de cada función que se engancha: puedes saber exactamente cuándo cada función se ingresa, pero sin conectar los puntos de salida no puede saber cuánto tiempo pasará hasta que reciba la siguiente información para atribuir al destinatario y cuánto a las personas que llaman.
Sin embargo, se ha demostrado que las herramientas de creación de perfiles de GNU son capaces de recopilar información de tiempo de ejecución para funciones en muchas plataformas, por lo que presumiblemente los desarrolladores tienen algún plan en mente para lograrlo.
He visto algunas implementaciones existentes que hacen cosas como mantener una pila de llamadas en la sombra y modificar la dirección del remitente al ingresar a __gnu_mcount_nc para que __gnu_mcount_nc sea invocado nuevamente cuando la persona que llama regrese; luego puede hacer coincidir el triplete de la persona que llama/llamada/sp con la parte superior de la pila de llamadas ocultas y así distinguir este caso de la llamada de entrada, registrar la hora de salida y regresar correctamente a la persona que llama.
Este enfoque deja mucho que desear:
- parece que puede ser frágil en presencia de recursividad y bibliotecas compiladas sin el indicador -pg
- parece que sería difícil de implementar con poca sobrecarga o en absoluto en entornos integrados de múltiples subprocesos/multinúcleos donde el soporte TLS de la cadena de herramientas está ausente y la identificación del subproceso actual puede ser costosa/compleja de obtener.
¿Existe alguna forma mejor y obvia de implementar __gnu_mcount_nc para que una compilación -pg pueda capturar la salida de la función y el tiempo de entrada que me falta?
gprof no usa esa función para cronometrar, de entrada o salida, sino para contar las llamadas de la función A que llama a cualquier función B. Más bien, usa el tiempo propio recopilado al contar muestras de PC en cada rutina, y luego usa la función: recuentos de llamadas a funciones para estimar cuánto de ese tiempo propio se debe cobrar a las personas que llaman.
Por ejemplo, si A llama a C 10 veces, B llama a C 20 veces y C tiene 1000 ms de tiempo propio (es decir, 100 muestras de PC), entonces gprof sabe que C ha sido llamado 30 veces y 33 de las muestras se pueden cargar a A, mientras que los otros 67 pueden cargarse a B. De manera similar, los recuentos de muestras se propagan hacia arriba en la jerarquía de llamadas.
Como puede ver, no funciona la entrada y salida del tiempo. Las mediciones que obtiene son muy aproximadas, porque no distingue entre llamadas cortas y llamadas largas. Además, si se produce una muestra de PC durante la E/S o en una rutina de biblioteca que no está compilada con -pg, no se cuenta en absoluto. Y, como notó, es muy frágil en presencia de recursividad y puede introducir una sobrecarga notable en funciones cortas.
Otro enfoque es el muestreo en pila, en lugar del muestreo en PC. Por supuesto, es más costoso capturar una muestra de pila que una muestra de PC, pero se necesitan menos muestras. Si, por ejemplo, una función, línea de código o cualquier descripción que quieras hacer es evidente en la fracción F del total de N muestras, entonces sabes que la fracción de tiempo que cuesta es F, con una desviación estándar. de raíz cuadrada (NF (1-F)). Entonces, por ejemplo, si toma 100 muestras y aparece una línea de código en 50 de ellas, entonces puede estimar los costos de la línea el 50% del tiempo, con una incertidumbre de sqrt(100*.5*.5) = +/- 5 muestras o entre 45% y 55%. Si toma 100 veces más muestras, puede reducir la incertidumbre en un factor de 10. (La recursividad no importa. Si una función o línea de código aparece 3 veces en una sola muestra, eso cuenta como 1 muestra, no 3). (Tampoco importa si las llamadas a funciones son cortas: si se llaman suficientes veces como para costar una fracción significativa, serán detectadas).
Tenga en cuenta que cuando busca cosas que pueda arreglar para acelerar, el porcentaje exacto no importa. Lo importante es encontrarlo. (De hecho, basta con ver un problema dos veces para saber que es lo suficientemente grande como para solucionarlo).
Esa es esta técnica .
PD: No se deje engañar por los gráficos de llamadas, las rutas de acceso o los puntos de acceso. Aquí hay un típico nido de ratas con gráficos de llamadas. El amarillo es el camino activo y el rojo es el punto activo.
Y esto muestra lo fácil que es que una jugosa oportunidad de aceleración no esté en ninguno de esos lugares:
Lo más valioso que hay que observar es una docena de muestras aleatorias de pilas sin procesar y relacionarlas con el código fuente. (Eso significa omitir el back-end del generador de perfiles).
AÑADIDO: Solo para mostrar lo que quiero decir, simulé diez muestras de pila del gráfico de llamadas anterior, y esto es lo que encontré
- 3/10 muestras llaman a
class_exists
, una con el fin de obtener el nombre de la clase y dos con el fin de establecer una configuración local.class_exists
llamaautoload
cuál llamarequireFile
, y dos de esos llamanadminpanel
. Si esto se pudiera hacer de forma más directa, se podría ahorrar alrededor del 30%. - 2/10 muestras están llamando
determineId
, que llama ,fetch_the_id
que llamagetPageAndRootlineWithDomain
, que llama a tres niveles más, terminando ensql_fetch_assoc
. Parece un montón de problemas para obtener una identificación, y cuesta alrededor del 20% del tiempo, y eso sin contar la E/S.
Entonces, los ejemplos de la pila no solo le dicen cuánto tiempo inclusive cuesta una función o línea de código, sino que también le dicen por qué se está haciendo y qué posible tontería se necesita para lograrlo. A menudo veo esto: generalidad galopante: aplastar moscas con martillos, no intencionalmente, sino simplemente siguiendo un buen diseño modular.
AÑADIDO: Otra cosa que no debe dejarse atrapar son los gráficos de llamas . Por ejemplo, aquí hay un gráfico de llamas (girado 90 grados a la derecha) de las diez muestras de pila simuladas del gráfico de llamadas anterior. Todas las rutinas están numeradas, en lugar de nombradas, pero cada rutina tiene su propio color.
Observe que el problema que identificamos anteriormente, con class_exists (rutina 219) en el 30% de las muestras, no es nada obvio al observar el gráfico de llama. Más muestras y colores diferentes harían que el gráfico se viera más "parecido a una llama", pero no expone rutinas que toman mucho tiempo al ser llamadas muchas veces desde diferentes lugares.
Aquí están los mismos datos ordenados por función en lugar de por tiempo. Eso ayuda un poco, pero no agrega similitudes llamadas desde diferentes lugares:
una vez más, el objetivo es encontrar los problemas que se te esconden. Cualquiera puede encontrar las cosas fáciles, pero los problemas que se esconden son los que marcan la diferencia.
AGREGADO: Otro tipo de atractivo visual es este:
donde las rutinas delineadas en negro podrían ser todas iguales, simplemente llamadas desde diferentes lugares. El diagrama no los agrega. Si una rutina tiene un alto porcentaje de inclusión al ser llamada una gran cantidad de veces desde diferentes lugares, no será expuesta.