El uso de Valgrind para analizar el código de las acciones hace que los programas sean más rápidos y menos potentes

¿Cuál es el momento adecuado para optimizar el código? Esta es una muy buena pregunta que generalmente se reduce a dos respuestas. La primera respuesta es tener un buen proyecto para comenzar con el código, porque "optimización" no significa "arreglar malas decisiones de proyecto". La segunda respuesta es que tiene que suceder después de que la aplicación haya sido suficientemente depurada y sus desarrolladores corran el riesgo de aburrirse.

También debería ser un objetivo para la optimización, basado en lo que tenga sentido para la aplicación. ¿Necesita procesar datos más rápido? ¿Envía menos datos a través de la red o al disco? ¿No deberíamos realmente mirar ese uso de memoria? ¿Y exactamente qué sucede dentro de esos cachés de CPU que hacen que el rendimiento a veces caiga por un precipicio en un kernel?

Todo esto y más se puede analizar utilizando herramientas de la serie Valgar, incluidas Cachegrind, Callgrind, DHAT y Massif.

Enfriando esos núcleos

Los procesadores contemporáneos están diseñados teniendo en cuenta un uso mínimo de energía, independientemente de si se dirigen a servidores, sistemas de escritorio o aplicaciones integradas. Esto básicamente significa que están en un estado de bajo consumo de energía cuando no están trabajando (bucle inactivo), con algunas CPU y microcontroladores que apagan una parte eléctrica del chip que no está en uso. Por lo tanto, cuanto más tenga que hacer el procesador, más energía utilizará y más calor recibirá.

El código que necesita menos instrucciones para realizar la misma tarea debido a un algoritmo más eficiente o menos abstracciones no solo funciona mejor, sino también más rápido. Esto significa que para el usuario la experiencia es que no solo la tarea se completa más rápido, sino que el dispositivo también está menos caliente, con menos ruido de ventilador. Si está alimentado por una batería, la batería también durará más con una carga. Básicamente, todos serán más felices.

Las armas elegidas aquí son Cachegrind y Callgrind. Aunque la creación de perfiles en masa (que se analiza más adelante en este artículo) también puede ser útil para ahorrar energía, el enfoque principal debe estar en el procesador. Esto significa que necesitamos saber qué hace nuestro código, especialmente en relación con qué partes de nuestro código funcionan con mayor frecuencia, ya que esos serían los objetivos principales de la optimización.

Seguimiento y seguimiento de esas llamadas

Ejecutar Cachegrind y Callgrind es bastante sencillo. Simplemente omita el nombre utilizable y las banderas a Valgrind junto con la herramienta que desea usar:

$ valgrind --tool=callgrind my_program 

Este comando lanzaría la herramienta Callgrind para nuestro programa nombrado mi programa. Opcionalmente, también podemos hacer que Callgrind simule los cachés de la CPU con --simulate-cache=yes. Durante la operación, Callgrind genera un archivo de salida llamado callgrind.out.<pid>, dónde <pid> es el identificador de proceso de la aplicación mientras se estaba ejecutando. Luego, este archivo se convierte a un formato legible por humanos:

$ callgrind_annotate callgrind.out. > callgrind00.txt

Esto produce un archivo que contiene (entre otras cosas) un resumen de la llamada a la función, alineado de acuerdo con el tiempo que se gastó una ejecución en esa función en particular, dejando en claro que se podría ganar mucha velocidad si esa función se optimizara.

Como se explica en este artículo en Stanford, el uso de la simulación de caché agrega detalles del éxito / fracaso de la caché:

  • Ir: Leo caché (instrucciones cumplidas)
  • I1mr: Errores de lectura de caché I1 (la instrucción no estaba en la caché I1 pero estaba en L2)
  • I2mr: La instrucción de caché L2 lee errores (la instrucción no estaba en la caché I1 o L2, tuvo que tomarse de la memoria)
  • Dr: D-cache lee (lecturas de memoria)
  • D1mr: Falta la lectura de la caché D1 (la ubicación de los datos no está en la caché D1, sino en L2)
  • D2mr: Falta la lectura de la caché L2 (la ubicación no está en D1 o L2)
  • Dw: Escritura en caché D (escritura en memoria)
  • D1mw: Falta escritura de caché D1 (no debe colocarse en la caché D1, sino en L2)
  • D2mw: Faltan datos de escritura de caché L2 (la ubicación no está en D1 o L2)
  • Ver una gran cantidad de cachés en un algoritmo o bucle sería una buena sugerencia para optimizarlo para ocupar menos datos en el caché, usar la captura previa para evitar cachés o tomar otros recursos aplicables al código en cuestión.

    El uso de Cachegrind es bastante similar a Callgrind, excepto que Cachegrind se centra principalmente en los cachés de la CPU, con características llamadas atención secundaria. Esto debería dejar en claro qué herramienta elegir de estas dos, dependiendo de las preguntas más urgentes de alguien.

    Haga más con menos memoria

    Aunque las computadoras e incluso los microcontroladores a menudo tienen más memoria caché y memoria principal del sistema (RAM) de lo que un programador de la década de 1990 podría soñar, hay dos aspectos negativos relacionados con la RAM:

    • La RAM no es infinita; en algún momento se acabará el espacio. Lo mejor de todo es que solo su aplicación termina desde el sistema operativo, no todo el sistema operativo (o RTOS) se rinde y provoca un bloqueo de fallas en todo el sistema.
    • La RAM activa cuesta energía. Cada parte de un módulo de RAM dinámica (DRAM) debe actualizarse constantemente para que se retengan las cargas capacitivas que retienen el valor. Se está volviendo especialmente importante para los dispositivos de batería.
    • Reducir la cantidad de memoria utilizada no solo afecta la RAM del sistema, sino que también ayuda con las cachés entre la CPU y las unidades de procesamiento de RAM. Menos datos significa menos cachés y menos demoras, ya que el sistema de memoria se apresura a mover los datos de RAM solicitados a la caché L3, L2 y (generalmente).

      Aunque los potentes procesadores de servidor Xeon o Epyc tienden a tener una caché L3 de 128 MB (o más), un procesador ARM de uso común como el de la Raspberry Pi 3 (el SoC BCM2837) tiene una caché L1 de 16 kB para datos e instrucciones cada uno, también caché L2 de 512 kB. Aquí no hay caché L3. A menos que su aplicación use menos de 512 kB de memoria en total (pila y pila), la RAM del sistema se verá afectada regularmente, lo que afectará en gran medida el rendimiento de la aplicación.

      Una distinción que se puede hacer aquí es que todas las aplicaciones tienden a tener datos almacenados en la RAM, ya sea en la pila o en la pila, a los que se puede acceder con regularidad o solo ocasionalmente. Al usar las herramientas DHAT y masivas de Valgrind, es bastante fácil averiguar los hábitos de uso de estos datos en la pila, así como qué datos no necesitan almacenarse en absoluto.

      Ejecutando los números

      Massive es la más fácil de usar de estas dos herramientas, solo necesita una llamada en la línea de comando:

$ valgrind --tool=massif my_program

Esto iniciará el programa y saldrá al archivo. massif.out.<pid>, dónde es el identificador de proceso de la aplicación mientras se ejecutaba. Antes de que podamos utilizar estos datos que Massive ha recopilado, primero debemos procesarlos:

$ ms_print massif.out.<pid> > massif00.txt

Esto dirigirá la salida de la utilidad ms_print a un archivo con los detalles en forma legible por humanos. Contiene un gráfico del uso masivo a lo largo del tiempo, como este ejemplo del documento Massive:

    MB
3.952^                                                                    # 
     |                                                                   @#:
     |                                                                 :@@#:
     |                                                            @@::::@@#: 
     |                                                            @ :: :@@#::
     |                                                          @@@ :: :@@#::
     |                                                       @@:@@@ :: :@@#::
     |                                                    :::@ :@@@ :: :@@#::
     |                                                    : :@ :@@@ :: :@@#::
     |                                                  :@: :@ :@@@ :: :@@#:: 
     |                                                @@:@: :@ :@@@ :: :@@#:::
     |                           :       ::         ::@@:@: :@ :@@@ :: :@@#:::
     |                        :@@:    ::::: ::::@@@:::@@:@: :@ :@@@ :: :@@#:::
     |                     ::::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:::::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                ::@@@: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |             :::::@ @: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |           @@:::::@ @: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
   0 +----------------------------------------------------------------------->Mi
     0                                                                   626.4

Number of snapshots: 63
 Detailed snapshots: [3, 4, 10, 11, 15, 16, 29, 33, 34, 36, 39, 41,
                      42, 43, 44, 49, 50, 51, 53, 55, 56, 57 (peak)]

El gráfico muestra el navegador Konquerer de KDE cuando se ha estado ejecutando y funcionando durante algún tiempo. El eje vertical muestra el uso masivo (en megabytes) y el eje horizontal el número de instrucciones ejecutadas desde que se lanzó la aplicación. De esta manera, puede comenzar a comprender cómo se ve el uso masivo, con cada uno de los cortes en el gráfico con más detalle en el archivo, p. Ej.

--------------------------------------------------------------------------------
  n        time(B)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
 15         21,112           19,096           19,000            96            0
 16         22,120           18,088           18,000            88            0
 17         23,128           17,080           17,000            80            0
 18         24,136           16,072           16,000            72            0
 19         25,144           15,064           15,000            64            0
 20         26,152           14,056           14,000            56            0
 21         27,160           13,048           13,000            48            0
 22         28,168           12,040           12,000            40            0
 23         29,176           11,032           11,000            32            0
 24         30,184           10,024           10,000            24            0
99.76% (10,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->79.81% (8,000B) 0x80483C2: g (example.c:5)
| ->39.90% (4,000B) 0x80483E2: f (example.c:11)
| | ->39.90% (4,000B) 0x8048431: main (example.c:23)
| |   
| ->39.90% (4,000B) 0x8048436: main (example.c:25)
|   
->19.95% (2,000B) 0x80483DA: f (example.c:10)
| ->19.95% (2,000B) 0x8048431: main (example.c:23)
|   
->00.00% (0B) in 1+ places, all below ms_print's threshold (01.00%)

La primera columna es el número de corte, con cortes detallados ampliados, que muestra el porcentaje de espacio de masa ocupado por datos específicos en la pila, así como en qué parte del código se asignó. Obviamente es necesario compilar la aplicación con los símbolos de depuración incluidos (opción -g para GCC) para aprovechar al máximo esta característica.

El uso de DHAT es similar a Massif, aunque sale al formato JSON, requiriendo el visor basado en navegador (dh_view.html) analizan realmente los datos. DHAT puede brindar información más detallada sobre los datos proporcionados en la pila que Massive, incluidas cosas como asignaciones que nunca se utilizan por completo. Si esto es necesario, depende de las optimizaciones que se deseen.

Completa esa caja

Después de mirar primero las otras herramientas de uso común en valgrind, debería tener una idea bastante clara de cuándo usar Valgrind para ayudar en la depuración y optimización. Aunque todas son herramientas increíblemente útiles, no son el final y son todos analizadores de depuración. Como todo programador experimentado sabe, lo importante es saber cuándo utilizar cada enfoque.

A veces, todo lo que se necesita es un depurador simple o una grabación sólida dentro de la aplicación para resaltar los problemas más importantes. Solo cuando eso no ayuda es el momento de comenzar a descomponer las herramientas más pesadas, con la advertencia de que las herramientas poderosas conllevan una gran responsabilidad para interpretar los datos. La experiencia y el conocimiento para tomar las decisiones correctas y sacar las conclusiones correctas es una herramienta tan esencial como cualquier otra.

  • Daños severos a los neumáticos dice:

    La mayoría de los códigos no necesitan ser optimizados.

    Lo que se puede hacer a menudo es tener las mayores mejoras con importantes cambios estratégicos, pero a veces la optimización radical puede tener ventajas. Por lo general, aunque es una pérdida de tiempo, guárdelo para cuando se aburra.

    • Brad Buhrkuhl dice:

      Esto es sorprendentemente incorrecto y muestra que nunca has trabajado en nada de ninguna escala o dificultad.

      • Nonny Moose dice:

        Me alegro de que los programas Chromium / Firefox no se suscriban a esa ideología.

      • Daños severos a los neumáticos dice:

        No, he estado involucrado durante más de 40 años y he pasado muchos de mis primeros años perdiendo el tiempo en la efectividad del código y defendiendo el lenguaje ensamblador como una cura para todas las dolencias.

        Primero hágalo bien, luego, si es necesario, hágalo rápidamente.

        • X dice:

          Mientras tanto, en el planeta tierra, los proveedores de bases de datos todavía encuentran que un gran enfoque en la optimización abre nuevos mercados para la analítica y crea nuevas y enormes oportunidades de ingresos.

      • Daños severos a los neumáticos dice:

        He aquí un ejemplo concreto. Un sistema RT integrado tiene un reloj que marca a 100 Hz. Cada tick debe hacer el mismo procesamiento y el código existente puede hacerlo en 8 ms, dejando 2 ms inactivos. Los requisitos no cambiarán. ¿Es útil optimizar ese código?

        Y otro: tengo un programa que se ejecuta una vez al día y genera un resumen. Tarda 10 segundos en funcionar. Mi programador junior me dice que ha estado ocupado toda la semana y ahora funciona en 5 segundos. ¿Qué le digo? ¡Estas despedido!

        • Tricon dice:

          Si bien un sistema integrado para un propósito particular puede no tener requisitos cambiantes, los requisitos cambian cuando los sistemas se reutilizan y se producen errores de reutilización. Estas mejoras pueden evitar que se produzcan errores en el futuro (sin embargo, también estoy de acuerdo en que es posible que no se detecten debido a esto). Por ejemplo, también es probable que las optimizaciones creen errores, p. Ej. Ariane 4 / Ariane 5.

          Le diría: "Un trabajo maravilloso. La optimización que ha logrado puede no ser útil en este caso en particular; sin embargo, comparta el conocimiento obtenido de este trabajo con otros para que podamos mejorar nuestras soluciones en el futuro. "

          • Steve Wheeler dice:

            No sé si lo despediría, pero ciertamente le diría que perdería su tiempo, tal vez el de los demás, y le costaría inútilmente a la empresa. Puede que tampoco haya adquirido conocimientos valiosos.

            Como ejemplo específico, a principios de la década de 1980, trabajé para NBI, que creó uno de los primeros sistemas de procesamiento de texto dedicados. Fue construido alrededor de un servidor central basado en 8086 con terminales basados ​​en 6800.

            Una vez que estaba destinado a resolver un problema con una determinada pieza de código en el servidor altamente optimizado, el desarrollador inicial pasó tres semanas averiguando lo que, según él, era la forma más efectiva de realizar la tarea. Me tomó tres semanas descubrir cómo funcionaban las cosas lo suficientemente bien como para arreglar el código. El ingeniero que solucionó un problema anterior con el código también tardó tres semanas en comprenderlo antes de aplicar su solución.

            El código optimizado ahorró unos cientos de milisegundos en comparación con una implementación simple. Se ejecutó una vez durante el inicio del sistema, que, para el servidor, probablemente fue una vez a la semana.

            Como otro ejemplo, en la década de 1990 en otra empresa, tuve que resolver un problema con un parachoques de anillo de puerta en serie en uno de nuestros productos: un cliente descubrió que un parachoques que volcaba causaba un estado de error constante en el que se colocaban letreros en el parachoques. dos veces. Cuando ingresé el código, encontré comentarios que elogiaban la solidez de la implementación, pero ningún comentario que describiera el código, ni sobre el proceso de pensamiento que llevó a la implementación de un búfer de anillo usando varios cientos de líneas de código para una máquina de estado con 64 estados. . .

            A algunos desarrolladores no les importan las justificaciones comerciales, los futuros cuidadores o cualquier otra cosa, solo quieren ser difíciles de exponer.

        • Somun dice:

          También debería haberse despedido porque no sabía en qué estaba trabajando su programador junior.

        • X dice:

          Ciertamente, hay un punto en la optimización del código si puede reducir la velocidad del reloj para reducir el consumo de energía o incluso reemplazar una porción de energía más baja.

          “Tengo un programa que se ejecuta una vez al día y genera algún tipo de resumen. Tarda 10 segundos en funcionar. Mi programador junior me dice que ha estado ocupado toda la semana y ahora funciona en 5 segundos. ¿Qué le digo? ¡Estas despedido! "

          Si realmente vendiera sus productos a los clientes, le daría dinero por ahorrarles tanto tiempo. Mi jefe definitivamente me daría un aumento si les ahorrara a cada uno de ellos 5 segundos a la semana.

  • DKE dice:

    Bueno, alguien tiene que publicarlo:

    "La optimización prematura es la raíz de todos los males" - Donald Knuth

    • adobebrendan dice:

      Es cierto, pero el código mal escrito sigue siendo una cloaca maloliente de la deuda técnica, incluso si no salen errores observables.

    • Claro dice:

      Ah, sí, un hombre que todavía usa el register palabra clave en C argumentando en contra de la optimización prematura. Siempre es bueno reírse de esa cita.

    • X dice:

      Bueno, alguien tiene que publicarlo:

      "No hay ninguna razón para que una persona tenga una computadora en su casa". - Ken Olsen

      "Linux es un cáncer que se adhiere intelectualmente a todo lo que toca", Steve Ballmer.

      Podemos fingir todo el día que estas personas eran "visionarias"

    • palanqueta dice:

      "Los desarrolladores pierden una gran cantidad de tiempo pensando o preocupándose por la velocidad de las partes no críticas de sus programas, y estas pruebas de rendimiento en realidad tienen un fuerte impacto negativo durante el debate y el mantenimiento. Tenemos que olvidarnos de las pequeñas eficiencias, digamos alrededor de 97 % del tiempo: la optimización prematura es la raíz de todos los males.

      Sin embargo, no perdamos nuestras oportunidades en ese crítico 3%. Un buen programador no se sentirá aliviado por este razonamiento, será prudente que observe detenidamente el código crítico; pero solo después de que se haya identificado ese código. "

      - Donald Knuth, 1974, "Programación estructurada con Go to Statements"

  • Werfu dice:

    Hoy en día, la mayoría de los desarrolladores confían en la asignación dinámica cuando los usaría con menos frecuencia. En primer lugar, porque la gestión de la memoria era inconveniente y era fácil filtrarla, pero principalmente porque las variables dinámicas no serían tan optimizadas (si es que lo hacían) por el compilador. Arreglar una ruta de ejecución con una variable estática es mucho más fácil para que el compilador optimice y mantenga esas variables dentro de los registros.

    • X dice:

      El uso de variables estáticas es una excelente manera de garantizar que su código nunca pase a un diseño moderno de múltiples subprocesos. Los diseñadores de procesadores se han esforzado mucho en darnos subprocesos, silenciamientos, etc. incluso en microcontroladores simples y los desarrolladores debemos aprovecharlos más.

  • pac dice:

    "Los procesadores contemporáneos están diseñados teniendo en cuenta el bajo consumo de energía ..."

    Mi caja Threadripper 2950X Gentoo quiere hablar contigo. 🙂

    • X dice:

      "Hola, mi eficiencia por vatio ha mejorado mucho que la de mis predecesores, por lo que puede hacer más con menos energía que nunca".

  • Ostraco dice:

    La optimización de los pozos puede tener importantes ventajas.

    https://techxplore.com/news/2020-05-early-bird-energy-deep-neural.html

  • Jonathan Bennett dice:

    Una ronda de desarrollo inicial de elaboración de perfiles puede revelar malas decisiones de proyectos que parecían buenas ideas en ese momento. Si espera hasta el final de un proyecto para ver la optimización, le costará más trabajo arreglar las cosas.

    Por otro lado, la optimización ajustada realmente buena necesita esperar. No tiene sentido optimizar el código que en realidad nunca funciona.

  • Forraje canónico dice:

    kcachegrind - ¡La herramienta que facilita la interpretación de 'callgrind'! Fácilmente disponible en github.

    No tengo ninguna afiliación con los creadores de esta herramienta, pero he usado esta herramienta muchas veces a lo largo de los años.
    Vale la pena todos los esfuerzos para instalar y operar. La interfaz de usuario, los gráficos y los gráficos facilitan la visualización de los puntos débiles del código.

    En cuanto a mi filosofía de "optimización", debe equilibrarse con "tiempo y lugar".
    * ¿Es el momento adecuado para optimizar?
    * ¿Es el lugar adecuado para optimizar?
    * ¿Cuál es el tiempo de un desarrollador (que es $) que está dispuesto a invertir?

    Debido a que la optimización es un proceso repetitivo, debe preguntarse repetidamente sobre su objetivo de optimización:
    * ¿Su optimización tiene como objetivo un retorno del 5% o del 25%?
    * ¿Dedica el 95% de su tiempo a ganar un 5%?

  • theRainHarvester en YouTube dice:

    Trabajé en sistemas de múltiples núcleos donde cada reloj importaba. La creación de perfiles con equipo personalizado fue lo primero que se creó. Por lo tanto, cada pequeño cambio logra el éxito y la mejora adicional se ha vuelto intuitiva.

  • Markus Wagner dice:

    Si alguien está interesado en medir el consumo de energía de las variantes de software en los teléfonos inteligentes modernos: ¡cuidado, hay dragones! https://arxiv.org/abs/2004.04500 (deriva del sensor, estados del sistema, ruido de otros procesos, ...)
    Algunos informes más en https://cs.adelaide.edu.au/~optlog/research/energy.php

  • Dave dice:

    Valgrind es una herramienta útil. Sin embargo, por el rendimiento que hice, descubrí que un perfil de ejecución es una mejor herramienta. Esto permite que el programa se ejecute a máxima velocidad en el procesador deseado, con interrupciones periódicas (cada pocos microsegundos) y el contador del programa registrado. Luego, los resultados se pueden calcular para determinar dónde pasa la mayor parte del tiempo el procesador.

    Una de las primeras discusiones sobre el analizador / perfilador de ejecución de programas fue realizada por Leigh Power en 1983, en el artículo de IBM System Journal, "Diseño y uso del analizador de ejecución de programas".

    https://dl.acm.org/doi/abs/10.5555/1717752.1717761

  • nnethercote dice:

    Si usa Massif, use el excelente https://github.com/KDE/massif-visualizer. Es una representación de los datos mucho mejor que la que se obtiene con el script ms_print predeterminado.

  • Steven Naslund dice:

    Optimizar su código es un poco de buena ciudadanía. Hoy en día hay tantos sistemas virtualizados que los pocos segundos que no le importaban privan a nadie más de esos ciclos informáticos. Hay algunas razones para asumir que su código siempre funcionará con hardware propietario.

Matías Jiménez
Matías Jiménez

Deja una respuesta

Tu dirección de correo electrónico no será publicada.