lunes, 15 de abril de 2013

Herramientas mágicas: Memcheck (de Valgrind)

La caja de herramientas básica de todo programador debe contener mínimamente un editor, un compilador y un depurador. Y si están empaquetados en su IDE de confianza mucho mejor. Pero además del kit básico, hay varias otras herramientas dando vueltas que pueden resultar muy muy útiles en muchos casos, y que conviene conocer. Sin embargo, no siempre se conocen, y por eso empiezo esta sección, para presentarles las que yo fui encontrando con el tiempo. Son herramientas que antes no extrañaba porque no sabía que existían, a las que tal vez no les encontré el potencial de entrada, pero que después de usarlas un tiempo y explorarlas mejor me parecieron geniales. Por esto, las terminé integrando en ZinjaI, mediante el menú genérico "Herramientas", donde pongo todo lo que no es básico e imprescindible para el alumno, pero sí útil para otros usuarios algo más exigentes.

En esta primera entrega voy a hablar de memcheck, una de las herramientas incluidas en un paquete más grande que se conoce como Valgrind. Valgrind es un framerwork para herramientas de análisis dinámico. Es decir, una infraestructura para analizar automáticamente cómo se comporta un programa cuando se ejecuta (de ahí el "dinámico"). Sobre esta infraestructura se construyen herramientas específicas, por eso cuando instalamos valgrind estamos instalando varias (6 al menos) herramientas que comparten una base común respecto a cómo ejecutan y analizan el programa, pero que centran su atención en partes diferentes, y por ello extraen información diferente. La más interesante de ellas es memcheck, que sirve para analizar cómo usa la memoria un programa, y detectar errores.


  

Para ser más claro, veamos un ejemplo. La herramienta se usa más o menos como un depurador, en el sentido de que tenemos que ejecutar el programa a través de la herramienta, y no como lo hacemos regularmente. Para un uso básico alcanza simplemente con agregar "valgrind" al principio de la linea de comandos. ZinjaI se encarga de ello si usamos la opción "Ejecutar" del submenú "Análisis dinámico" del menú "Herramientas". Cuando nuestro programa termine, Valgrind generará un reporte con los problemas que observó durante la ejecución. En el caso de memcheck, lo que hace es llevar registro de toda la memoria que usa el programa, de qué lugares han sido asignados/inicializados, del tamaño de los arreglos, etc. Esto le permite detectar cuando accedemos a lugares que no corresponde, o qué bloques no son liberados correctamente (memory leaks y similares). En el ejemplo de la siguiente imágen hay un código lleno de errores:


A la izquierda está el panel de resultados de Valgrind (suele aparecer abajo, pero lo moví para que se vea mejor). Muestra su informe, donde indica que detectó varias cosas: que en la linea 8 escribimos en una posición de memoria que no corresponde (y aclara que se sale del  arreglo reservado en la linea 6); que en la linea 10 hay un if que usa en su condición una variable a la que nunca le asignamos nada, y entonces tendrá "basura"; que el "delete d" de la linea 16 no coincide con el "d=new..." de la linea 6 (faltan corchetes); y que nuestro programa llegó a usar 80 bytes de memoria dinámica, y que 40 de esos nunca fueron liberados (falta un delete para un new, se indica cual). En un código simple como este, donde todo ocurre dentro de una única función, es relativamente fácil notar esos errores sin Memcheck (es más, uno de ellos sale como warning al compilar). Pero en programas más complejos, donde la memoria va y viene entre varias funciones, clases, se usa en distintos lugares de distintos fuentes, etc, rastrear estas cosas es casi imposible. Y valgrind lo hace siempre igual de fácil.

Pero no todo son rosas. Hay dos grandes problemas con Valgrind. El primero es que no está disponible para Windows, así que ZinjaI solo mostrará estas opciones en el menú Herramientas si estamos en GNU/Linux. Hay alternativas similares para Windows, pero no he probado ninguna, ya que hasta donde escuché, las que mejor andan son comerciales. De hecho, he leido alguna vez acerca de proyectos comerciales para plataformas Windows, en los cuales se desarrollaron internamente (no se publicaron) ports a GNU/Linux solo para poder testearlos con Valgrind.

La segunda contra de Valgrind es su velocidad. Valgrind funciona de forma similar a una máquina virtual, introduciendo toda una capa de emulación entre el programa y el procesador. Es en esa capa en donde analiza lo que ocurre con las instrucciones del programa, pudiendo registrar las direcciones de memoria, simular mecanismos de cache para analizar sus fallos, contabilizar bloqueos en programas paralelos, y otras magias dependiendo de cual de sus herramientas usemos. Esta capa significa una pérdida de rendimiento, y esa pérdida es bastante notable. El programa corre mucho más lento en Valgrind, respecto a lo que correría normalmente, y en algunos casos esto limita un poco su aplicación, pero en la mayoría no.

Finalmente, como tercer pseudoproblema podemos mencionar el uso de bibliotecas externas. Al usar bibliotecas externas valgrind detectará todo tipo de errores dentro de esas bibliotecas que ensuciarán la salida agregando entre la lista de resultados muchas cosas que no son de nuestro interés. Por ejemplo, en aplicaciones OpenGL he llegado a ver errores que ocurren dentro de los drivers de video. Dependiendo de la biblioteca, habrá más o menos errores. Valgrind tiene su mecanismos para indicar cuales deben ignorarse en el reporte y cuales no. La forma más simple consiste en pasarle un archivo de texto con reglas que indican qué ignorar. Todavía no estudié en detalle estos mecanismos, pero espero incluir un soporte al menos básico para esto en la próxima versión de ZinjaI (esto es poder especificar el archivo con las reglas, y poder agregar una regla seleccionando el error y haciendo click derecho por ejemplo).

Por último, vale decir que usar Valgrind también nos incentiva a tener buenas prácticas de programación. Por ejemplo, uno muchas veces no hace ciertos deletes, porque corresponden a bloques de memoria que viven durante toda la ejecución del programa y solo se liberan cuando este termina, y en ese punto, si no hicimos el "delete", el sistema operativo se encargará igual de limpiar por nosotros. Por eso, muchas veces los omitimos, pero si usamos Valgrind estos "errores" saldrán en el reporte generando ruido, así que nos convendrá corregirlos aunque no impacten en el funcionamiento de nuestra aplicación. Si intentan usar Valgrind con un proyecto más o menos maduro en el que nunca hicieron este tipo de análisis verán de que hablo.

En conclusión, creo que esta es una de las más mágicas de las herramientas mágicas que voy a ir mencionando. Realmente es útil para complementar al depurador, y a mí me ha permitido encontrar errores que de otra forma no habría podido encontrar nunca, ya que como dije antes, por su forma de hacerlo, a Valgrind no le hace ninguna diferencia que yo ejecute un ejemplo simple como el de la imagen, o un proyecto entero como PSeInt.

No hay comentarios:

Publicar un comentario