Pregunta ¿Cómo puedo perfilar el código de C ++ que se ejecuta en Linux?


Tengo una aplicación C ++, ejecutándose en Linux, que estoy en proceso de optimizar. ¿Cómo puedo identificar qué áreas de mi código se están ejecutando lentamente?


1498
2017-12-17 20:29


origen


Respuestas:


Si su objetivo es usar un generador de perfiles, use uno de los sugeridos.

Sin embargo, si tiene prisa y puede interrumpir manualmente su programa bajo el depurador mientras es subjetivamente lento, hay una manera simple de encontrar problemas de rendimiento.

Simplemente deténgala varias veces, y cada vez mire la pila de llamadas. Si hay algún código que está desperdiciando algún porcentaje del tiempo, 20% o 50% o lo que sea, esa es la probabilidad de que lo atrape en el acto en cada muestra. Entonces ese es aproximadamente el porcentaje de muestras en que lo verá. No se requieren conjeturas educadas. Si tiene una conjetura sobre cuál es el problema, esto lo probará o lo desmentirá.

Puede tener problemas de rendimiento múltiple de diferentes tamaños. Si elimina uno de ellos, los restantes tomarán un porcentaje mayor y serán más fáciles de detectar en pases posteriores. Esta efecto de aumento, cuando se combina con múltiples problemas, puede conducir a factores de aceleración verdaderamente masivos.

Advertencia: los programadores tienden a ser escépticos de esta técnica a menos que la hayan utilizado ellos mismos. Dirán que los perfiladores le brindan esta información, pero eso solo es cierto si toman muestras de la pila de llamadas completa y luego le permiten examinar un conjunto aleatorio de muestras. (Los resúmenes son donde se pierde la idea.) Los gráficos de llamadas no le dan la misma información, porque

  1. no resumen en el nivel de instrucción, y
  2. dan resúmenes confusos en presencia de recursión.

También dirán que solo funciona en programas de juguetes, cuando en realidad funciona en cualquier programa, y ​​parece funcionar mejor en programas más grandes, porque tienden a tener más problemas para encontrar. Dirán que a veces encuentran cosas que no son problemas, pero eso solo es cierto si ves algo una vez. Si ve un problema en más de una muestra, es real.

PD Esto también se puede hacer en programas de subprocesos múltiples si hay una manera de recolectar muestras de pila de llamadas del grupo de subprocesos en un punto en el tiempo, como ocurre en Java.

P.P.S Como generalización aproximada, cuantas más capas de abstracción tenga en su software, más probabilidades tendrá de encontrar que esa es la causa de los problemas de rendimiento (y la oportunidad de acelerar).

Agregado: Puede que no sea obvio, pero la técnica de muestreo de pila funciona igual de bien en presencia de recursión. La razón es que el tiempo que se ahorraría mediante la eliminación de una instrucción se aproxima a la fracción de muestras que lo contiene, independientemente de la cantidad de veces que pueda ocurrir dentro de una muestra.

Otra objeción que escucho a menudo es: "Se detendrá en algún lugar al azar y se perderá el problema real". Esto proviene de tener un concepto previo de cuál es el problema real. Una propiedad clave de los problemas de rendimiento es que desafían las expectativas. El muestreo le dice que algo es un problema, y ​​su primera reacción es de incredulidad. Eso es natural, pero puede estar seguro si encuentra un problema, es real, y viceversa.

AÑADIDO: Déjame hacer una explicación Bayesiana de cómo funciona. Supongamos que hay alguna instrucción I (llamada o no) que está en la pila de llamadas una fracción f del tiempo (y por lo tanto cuesta tanto). Por simplicidad, supongamos que no sabemos qué f es, pero suponga que es 0.1, 0.2, 0.3, ... 0.9, 1.0, y la probabilidad previa de cada una de estas posibilidades es 0.1, por lo que todos estos costos son igualmente probables a-priori.

Entonces supongamos que tomamos solo 2 muestras de pila, y vemos instrucciones I en ambas muestras, observación designada o=2/2. Esto nos da nuevas estimaciones de la frecuencia f de I, de acuerdo a esto:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

La última columna dice que, por ejemplo, la probabilidad de que f > = 0.5 es 92%, arriba de la suposición anterior de 60%.

Supongamos que las suposiciones anteriores son diferentes. Supongamos que suponemos que P (f = 0.1) es .991 (casi cierto), y todas las otras posibilidades son casi imposibles (0.001). En otras palabras, nuestra certeza previa es que I es barato. Entonces obtenemos:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Ahora dice que P (f> = 0.5) es 26%, arriba de la suposición anterior de 0.6%. Así que Bayes nos permite actualizar nuestra estimación del costo probable de I. Si la cantidad de datos es pequeña, no nos dice exactamente cuál es el costo, solo que es lo suficientemente grande como para valer la pena.

Sin embargo, otra forma de verlo se llama Regla de Sucesión. Si lanza una moneda dos veces, y sale cara ambas veces, ¿qué le dice esto sobre la posible ponderación de la moneda? La forma respetada de responder es decir que es una distribución Beta, con un valor promedio (número de visitas + 1) / (número de intentos + 2) = (2 + 1) / (2 + 2) = 75%.

(La clave es que vemos I mas de una vez. Si solo lo vemos una vez, eso no nos dice mucho, excepto que f > 0.)

Entonces, incluso un número muy pequeño de muestras puede decirnos mucho sobre el costo de las instrucciones que ve. (Y los verá con una frecuencia, en promedio, proporcional a su costo. n se toman muestras, y f es el costo, entonces I aparecerá en nf+/-sqrt(nf(1-f)) muestras. Ejemplo, n=10, f=0.3, es decir 3+/-1.4 muestras)


AGREGADO, para dar una sensación intuitiva de la diferencia entre la medición y el muestreo aleatorio de la chimenea:
Ahora hay perfiladores que muestrean la pila, incluso en la hora del reloj de pared, pero lo que sale Son mediciones (o ruta caliente, o punto caliente, desde el cual un "cuello de botella" puede ocultarse fácilmente). Lo que no te muestran (y lo podrían hacer fácilmente) son las muestras mismas. Y si tu objetivo es encontrar el cuello de botella, el número de ellos que necesita ver es de media, 2 dividido por la fracción de tiempo que lleva. Entonces, si toma el 30% de tiempo, 2 / .3 = 6.7 muestras, en promedio, lo mostrarán, y la probabilidad de que 20 muestras muestren que es 99.2%.

Aquí hay una ilustración de la diferencia entre examinar las mediciones y examinar las muestras de la pila. El cuello de botella puede ser una gran mancha como esta, o muchas pequeñas, no hace ninguna diferencia.

enter image description here

La medida es horizontal; te dice qué fracción de tiempo toman las rutinas específicas. El muestreo es vertical. Si hay alguna forma de evitar lo que todo el programa está haciendo en ese momento, y si lo ves en una segunda muestra, has encontrado el cuello de botella. Eso es lo que marca la diferencia: ver la razón completa del tiempo que se está gastando, no solo cuánto.


1192
2018-04-21 04:09



Puedes usar Valgrind con las siguientes opciones

valgrind --tool=callgrind ./(Your binary)

Generará un archivo llamado callgrind.out.x. Puedes usar kcachegrind herramienta para leer este archivo. Le dará un análisis gráfico de las cosas con resultados, como qué líneas cuestan cuánto.


472
2017-12-17 20:34



Supongo que estás usando GCC. La solución estándar sería hacer un perfil con gprof.

Asegúrese de agregar -pg compilar antes de perfilar:

cc -o myprog myprog.c utils.c -g -pg

No lo he probado todavía, pero he oído cosas buenas sobre google-perftools. Definitivamente vale la pena intentarlo.

Pregunta relacionada aquí.

Algunas otras palabras de moda si gprof no hace el trabajo por usted: ValgrindIntel VTune, Sol DTrace.


296
2017-08-17 11:48



Los kernels más nuevos (por ejemplo, los últimos núcleos de Ubuntu) vienen con las nuevas herramientas 'perf' (apt-get install linux-tools) AKA perf_events.

Estos vienen con perfiladores de muestreo clásicos (man-page) así como el impresionante tabla de tiempo!

Lo importante es que estas herramientas pueden ser perfil del sistema y no solo de perfiles de procesos: pueden mostrar la interacción entre subprocesos, procesos y kernel y le permiten comprender la programación y las dependencias de E / S entre procesos.

Alt text


216
2018-05-22 21:44



Utilizaría Valgrind y Callgrind como base para mi conjunto de herramientas de creación de perfiles. Lo que es importante saber es que Valgrind es básicamente una máquina virtual:

(wikipedia) Valgrind es, en esencia, un virtual   máquina que utiliza just-in-time (JIT)   técnicas de compilación, que incluyen   recompilación dinámica Nada de   el programa original alguna vez se ejecuta   directamente en el procesador host.   En cambio, Valgrind primero traduce el   programa en una forma temporal, más simple   llamada Representación Intermedia   (IR), que es un procesador neutral   Formulario basado en SSA. Después de la conversión,   una herramienta (ver abajo) es libre de hacer   cualesquiera transformaciones que le gustaría   en el IR, antes de que Valgrind traduzca   el IR de nuevo en el código de la máquina y permite   el procesador host lo ejecuta.

Callgrind es un generador de perfiles sobre eso. El principal beneficio es que no tiene que ejecutar su aplicación durante horas para obtener un resultado confiable. Incluso una segunda ejecución es suficiente para obtener resultados sólidos y confiables, porque Callgrind es un no sondear perfilador.

Otra herramienta construida sobre Valgrind es Massif. Lo uso para perfilar el uso de memoria de montón. Funciona muy bien. Lo que hace es que le da instantáneas del uso de la memoria: información detallada ¿QUÉ tiene QUÉ porcentaje de memoria, y QUIÉN la puso allí? Dicha información está disponible en diferentes momentos de ejecución de la aplicación.


65
2018-06-30 19:30



Esta es una respuesta a La respuesta de Gprof de Nazgob.

He estado usando Gprof en los últimos días y ya he encontrado tres limitaciones importantes, una de las cuales no he visto documentada en ningún otro lado (todavía):

  1. No funciona correctamente en código de subprocesos múltiples, a menos que use un solución alternativa

  2. El gráfico de llamadas se confunde con los punteros de función. Ejemplo: Tengo una función llamada multithread () que me permite realizar múltiples subprocesos en una función específica sobre una matriz especificada (ambos pasados ​​como argumentos). Sin embargo, Gprof considera todas las llamadas a multithread () como equivalentes a los efectos del tiempo de cálculo en niños. Como algunas funciones que paso a multithread () tardan mucho más que otras, mis gráficos de llamada son en su mayoría inútiles. (Para aquellos que se preguntan si el tema es el enhebrado aquí: no, multithread () puede, opcionalmente, y lo hizo en este caso, ejecutar todo de forma secuencial solamente en el hilo de llamada).

  3. Dice aquí que "... las cifras de número de llamadas se obtienen por conteo, no por muestreo. Son completamente precisas ...". Sin embargo, creo que mi gráfico de llamadas me da 5345859132 + 784984078 como estadísticas de llamadas a mi función más llamada, donde se supone que el primer número son llamadas directas, y las segundas llamadas recursivas (que son todas de sí mismo). Como esto implicaba que tenía un error, puse contadores largos (de 64 bits) en el código e hice lo mismo de nuevo. Mis recuentos: 5345859132 directos y 78094395406 llamadas recursivas. Hay muchos dígitos allí, así que señalaré que las llamadas recursivas que mido son 78 mil millones, contra 784 millones de Gprof: un factor de 100 diferentes. Ambas ejecuciones eran de un solo subproceso y código sin optimizar, una compilada -g y la otra -pg.

Esto fue GNU Gprof (GNU Binutils para Debian) 2.18.0.20080103 ejecutando bajo Debian Lenny de 64 bits, si eso ayuda a alguien.


49
2018-06-08 08:01



La respuesta para correr valgrind --tool=callgrind no está del todo completo sin algunas opciones. Por lo general, no queremos perfilar 10 minutos de tiempo de inicio lento en Valgrind y queremos crear un perfil de nuestro programa cuando se está realizando alguna tarea.

Entonces esto es lo que recomiendo Ejecuta el programa primero:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

Ahora cuando funciona y queremos comenzar a perfilar, debemos ejecutarlo en otra ventana:

callgrind_control -i on

Esto activa los perfiles. Para apagarlo y detener toda la tarea, podríamos usar:

callgrind_control -k

Ahora tenemos algunos archivos llamados callgrind.out. * En el directorio actual. Para ver los resultados de los perfiles, use:

kcachegrind callgrind.out.*

Recomiendo en la siguiente ventana hacer clic en el encabezado de la columna "Auto", de lo contrario, muestra que "main ()" es la tarea que más tiempo consume. "Self" muestra cuánto tomó cada función en sí misma, no junto con dependientes.


47
2018-02-23 21:28



Use Valgrind, callgrind y kcachegrind: 

valgrind --tool=callgrind ./(Your binary)

genera callgrind.out.x. Léelo usando kcachegrind.

Use gprof (agregar -pg): 

cc -o myprog myprog.c utils.c -g -pg 

(no tan bueno para multi-threads, punteros de función)

Use google-perftools: 

Utiliza el muestreo de tiempo, revela los cuellos de botella de E / S y CPU que se revelan.

Intel VTune es el mejor (gratuito para fines educativos).

Otros: AMD Codeanalyst, OProfile, herramientas 'perf' (apt-get install linux-tools)


8
2018-03-17 12:20