¿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?

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

Sin embargo, si tiene prisa y puede interrumpir manualmente su progtwig 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 stack 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. Este efecto de ampliación , cuando se combina con múltiples problemas, puede conducir a factores de aceleración verdaderamente masivos.

Advertencia: los progtwigdores 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 stack 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 progtwigs de juguetes, cuando en realidad funciona en cualquier progtwig, y ​​parece funcionar mejor en progtwigs más grandes, porque tienden a tener más problemas para encontrar. Dirán que a veces encuentra 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 progtwigs de subprocesos múltiples si hay una manera de recostackr muestras de la stack de llamadas del grupo de subprocesos en un punto en el tiempo, como ocurre en Java.

PPS 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 stack 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 verdadero problema “. 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 u otra) que está en la stack de llamadas alguna fracción f del tiempo (y por lo tanto cuesta tanto). Por simplicidad, supongamos que no sabemos qué es f , pero supongamos 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 probablemente a-priori.

Entonces supongamos que tomamos solo 2 muestras de stack, y vemos la instrucción I en ambas muestras, observación designada o=2/2 . Esto nos da nuevas estimaciones de la frecuencia f de I , de acuerdo con 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 sea 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 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%. Entonces, 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 bashs + 2) = (2 + 1) / (2 + 2) = 75%.

(La clave es que lo vemos más 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. Si se toman n muestras, y f es el costo, entonces apareceré en muestras nf+/-sqrt(nf(1-f)) . , n=10 , f=0.3 , eso es 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 stack, incluso en la hora del reloj de pared, pero lo que sale son medidas (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 su objective es encontrar el cuello de botella, el número de ellos que necesita ver es, en promedio , 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 stack. 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 manera de evitar lo que está haciendo todo el progtwig 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.

Puede usar Valgrind con las siguientes opciones

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

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

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

Asegúrese de agregar -pg a la comstackción antes del perfilado:

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

Todavía no lo he probado, pero he escuchado 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: Valgrind , Intel VTune , Sun DTrace .

Los núcleos 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 los perfiles de muestreo clásicos ( página de manual ), así como con el increíble cronogtwig !

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

texto alternativo

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, una máquina virtual que utiliza técnicas de comstackción justo a tiempo (JIT), incluida la recomstackción dinámica. Nada del progtwig original se ejecuta directamente en el procesador host. En cambio, Valgrind primero traduce el progtwig en una forma temporal más simple llamada Representación intermedia (IR), que es una forma basada en SSA, neutral para el procesador. Después de la conversión, una herramienta (ver abajo) es libre de hacer las transformaciones que quiera en el IR, antes de que Valgrind traduzca el IR al código de la máquina y permita que el procesador del host lo ejecute.

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 fiables y sólidos como una roca, porque Callgrind es un perfilador que no investiga .

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.

Esta es una respuesta a la respuesta 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 utilice una 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 contando, no muestreando. 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 comstackda -g y la otra -pg.

Este fue GNU Gprof (GNU Binutils para Debian) 2.18.0.20080103 que se ejecuta bajo Debian Lenny de 64 bits, si eso ayuda a alguien.

La respuesta para ejecutar valgrind --tool=callgrind no está del todo completa 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 progtwig cuando se está realizando alguna tarea.

Entonces esto es lo que recomiendo Ejecuta el progtwig 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.

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)

Para progtwigs de subproceso único, puede usar igprof , The Ignominous Profiler: https://igprof.org/ .

Es un generador de perfiles de muestreo, en la línea de la … larga … respuesta de Mike Dunlavey, que empaquetará los resultados en un árbol de stack de llamadas navegable, anotado con el tiempo o la memoria gastados en cada función, ya sea acumulativo o por función.

Estos son los dos métodos que uso para acelerar mi código:

Para aplicaciones vinculadas a la CPU:

  1. Utilice un generador de perfiles en el modo DEPURAR para identificar partes cuestionables de su código
  2. Luego cambie al modo RELEASE y comente las secciones cuestionables de su código (córtelo sin nada) hasta que vea cambios en el rendimiento.

Para aplicaciones vinculadas de E / S:

  1. Use un generador de perfiles en modo RELEASE para identificar partes cuestionables de su código.

nótese bien

Si no tiene un generador de perfiles, use el generador de perfiles del pobre. Haga clic en pausa mientras depura su aplicación. La mayoría de las suites de desarrolladores se romperán en ensamble con los números de línea comentados. Es probable que aterrice estadísticamente en una región que está consumiendo la mayoría de sus ciclos de CPU.

Para la CPU, la razón para crear perfiles en el modo DEPURAR es porque si pruebas tu perfil en modo RELEASE , el comstackdor reducirá las operaciones matemáticas, vectorizar bucles y en línea, lo que tiende a englobar tu código en un lío imposible de mapear cuando está ensamblado. Un desorden imposible de mapear significa que su perfilador no podrá identificar claramente lo que lleva tanto tiempo porque el ensamblaje puede no corresponderse con el código fuente en optimización . Si necesita el rendimiento (por ejemplo, temporización sensible) del modo RELEASE , desactive las funciones del depurador según sea necesario para mantener un rendimiento útil.

Para la vinculación de E / S, el generador de perfiles aún puede identificar operaciones de E / S en modo RELEASE porque las operaciones de E / S están vinculadas externamente a una biblioteca compartida (la mayoría de las veces) o, en el peor de los casos, dará como resultado un sistema vector de interrupción de llamada (que también es fácilmente identificable por el generador de perfiles).