Tiempo de salida de la función de captura con __gnu_mcount_nc

Estoy tratando de hacer algunos perfiles de rendimiento en una plataforma incrustada prototipo con poca asistencia.

Observo que el indicador de CPG de GCC hace que thunks a __gnu_mcount_nc se inserten en la entrada de cada función. No hay implementación disponible de __gnu_mcount_nc (y el proveedor no está interesado en ayudar), sin embargo, como es trivial escribir uno que simplemente registre el recuento de la stack y el ciclo actual, lo he hecho; esto funciona bien y está produciendo resultados útiles en términos de gráficos de llamadas / llamadas y más frecuentemente llamadas funciones.

También me gustaría obtener información sobre el tiempo pasado en cuerpos funcionales, sin embargo, tengo dificultades para entender cómo abordar esto con solo la entrada, pero no la salida, para que cada función se enganche: se puede decir exactamente cuándo cada función se ingresa, pero sin conectar los puntos de salida no se puede saber la cantidad de tiempo hasta que reciba la siguiente información para atribuirle al destinatario y cuánto a los llamantes.

Sin embargo, las herramientas de creación de perfiles de GNU son, de hecho, demostrablemente capaces de reunir 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 lograr esto.

He visto algunas implementaciones existentes que hacen cosas como mantener una stack de llamadas en la sombra y mezclar la dirección de retorno en la entrada a __gnu_mcount_nc para que __gnu_mcount_nc se invoque de nuevo cuando el destinatario vuelva; luego puede unir la tripleta caller / callee / sp contra la parte superior de la stack de llamadas shadow y así distinguir este caso de la llamada a la entrada, registrar el tiempo de salida y devolverlo correctamente a la persona que llama.

Este enfoque deja mucho que desear:

  • parece que puede ser frágil en presencia de recursión y bibliotecas comstackdas sin la bandera -pg
  • parece que sería difícil de implementar con poca sobrecarga o en entornos embebidos multiproceso / multinúcleo donde el soporte TLS de la cadena de herramientas está ausente y la identificación del subproceso actual puede ser costosa / compleja de obtener

¿Hay alguna forma mejor y más obvia de implementar __gnu_mcount_nc para que una versión de glp sea capaz de capturar la salida de la función, así como el tiempo de entrada que me falta?

gprof no usa esa función para el tiempo, de entrada o salida, sino para el conteo de llamadas de la función A que llama a cualquier función B. Más bien, usa el auto-tiempo reunido contando muestras de PC en cada rutina, y luego usa la función- conteos de llamadas a función para estimar cuánto de ese auto-tiempo debería ser cargado a las personas que llaman.

Por ejemplo, si A llama C 10 veces, y B llama C 20 veces y C tiene 1000 ms de tiempo propio (es decir, 100 muestras de PC), entonces gprof sabe que se ha llamado C 30 veces, y 33 de las muestras se pueden cargar a A, mientras que los otros 67 se pueden cargar a B. De manera similar, los recuentos de muestras se propagan por la jerarquía de llamadas.

Como ve, no funciona el tiempo de entrada y salida. Las mediciones que obtiene son muy generales, porque no hace distinción entre llamadas cortas y llamadas largas. Además, si una muestra de PC ocurre durante E / S o en una rutina de biblioteca que no se comstack con -pg, no se cuenta en absoluto. Y, como ha notado, es muy frágil en presencia de recursión, y puede introducir una sobrecarga notable en funciones cortas.

Otro enfoque es el muestreo de stack, en lugar del muestreo de PC. Por supuesto, es más costoso capturar una muestra de stack 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 desee realizar, es evidente en la fracción F del total de N muestras, entonces sabrá que la fracción de tiempo que cuesta es F, con una desviación estándar de sqrt (NF (1-F)). Entonces, por ejemplo, si toma 100 muestras y aparece una línea de código en 50 de ellas, puede estimar que la línea cuesta 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 por 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 función son cortas; si se las llama las suficientes veces para que les cueste una fracción significativa, serán atrapadas).

Tenga en cuenta que cuando busca cosas que puede arreglar para acelerar, el porcentaje exacto no importa. Lo importante es encontrarlo. (De hecho, solo necesita 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 en caliente o los puntos conflictivos. Aquí hay un nido de rata típico de call-graph. El amarillo es el camino caliente, y el rojo es el punto caliente.

enter image description here

Y esto muestra lo fácil que es para una jugosa oportunidad de aceleración estar en ninguno de esos lugares:

enter image description here

Lo más valioso que hay que mirar es una docena de muestras de stack crudas aleatorias y relacionarlas con el código fuente. (Eso significa pasar por alto el backend del generador de perfiles).

AGREGADO: Solo para mostrar lo que quiero decir, simulé diez muestras de stack del gráfico de llamadas de arriba, y esto es lo que encontré

  • 3/10 muestras están llamando a class_exists , una con el propósito de obtener el nombre de la clase, y dos con el propósito de establecer una configuración local. class_exists llama a autoload las llamadas requireFile , y dos de ellas llaman a adminpanel . Si esto se puede hacer más directamente, podría ahorrar alrededor del 30%.
  • 2/10 muestras están llamando a determineId , que llama a fetch_the_id que llama a getPageAndRootlineWithDomain , que llama a tres niveles más, terminando en sql_fetch_assoc . Parece un montón de problemas para obtener una ID, y está costando aproximadamente el 20% del tiempo, y eso sin contar la E / S.

Por lo tanto, las muestras de stack no solo le dicen cuánto tiempo incluye una función o una línea de código, sino que le dicen por qué se está haciendo y qué posible tontería se necesita para lograrlo. A menudo veo esto -generalidad galopante- golpear moscas con martillos, no intencionalmente, sino siguiendo un buen diseño modular.

AÑADIDO: Otra cosa para no ser absorbido es gráficos de llamas . Por ejemplo, aquí hay un gráfico de llama (girado 90 grados a la derecha) de las diez muestras de stack simuladas del gráfico de llamadas anterior. Las rutinas están numeradas, en lugar de nombradas, pero cada rutina tiene su propio color. enter image description here
Observe que el problema que identificamos anteriormente, con class_exists (rutina 219) en el 30% de las muestras, no es del todo obvio al observar el gráfico de llama. Más muestras y diferentes colores harían que el gráfico pareciera más “similar 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: enter image description here
Una vez más, el objective es encontrar los problemas que se esconden de usted. Cualquiera puede encontrar lo fácil, pero los problemas que se esconden son los que marcan la diferencia.

AGREGADO: Otro tipo de ojos dulces es este:
enter image description here donde las rutinas delineadas en negro podrían ser todas iguales, simplemente llamadas desde diferentes lugares. El diagtwig no los agrega por ti. Si una rutina tiene un alto porcentaje inclusivo al ser llamado un gran número de veces desde diferentes lugares, no estará expuesto.