Revista Do Linux
 
OUTRAS SEÇÕES
  Cartas
  Variedades
  Rádio Linux
  Mensagem ao Leitor
  CD do Mês
  Coluna do Augusto
  Leitura
  Dicas e truques
  Opinião
 

Memória perdida

Muita gente diz que não gosta de linguagens como a C e a C++ por causa da complexidade de conceitos como ponteiros. Embora se possa argumentar que os ponteiros são bem mais simples do que parecem à primeira vista, a verdade é que eles são uma dor de cabeça para qualquer pessoa preocupada com o desempenho de um sistema. Quando falo em ~Sdesempenho~T, não estou apenas falando sobre velocidade inicial, mas de consumo de memória, o que sempre acaba reduzindo a velocidade no final.

A maior causa de problemas de consumo abusivo de memória são os chamados memory leaks (ML), ou vazamentos de memória. Os MLs acontecem quando, por uma razão qualquer, uma região de memória foi alocada e não liberada posteriormente. A alocação e desalocação de memória é relativamente fácil de controlar em programas pequenos, mas conforme a complexidade do software aumenta, aumenta também a dificuldade de se controlar a memória utilizada.

Até pouco tempo, a grande maioria dos programadores não tinha muita opção além de ler o código linha a linha para tentar achar o vazamento. Isto era extremamente ineficiente, além de muito cansativo.

Até que Julian Seward lançou o Valgrind, um depurador de memória para Linux em plataforma x86. O Valgrind é um depurador extremamente competente e já teve sua eficácia comprovada em diversos projetos grandes como, por exemplo, o KDE. Atualmente, é procedimento padrão no projeto KDE a utilização do Valgrind em todos os aplicativos que fazem parte do conjunto.

Faça o download dos fontes do Valgrind e copie-os para o diretório onde você os irá compilar. Por exemplo, copie o arquivo para o diretório /tmp e execute os passos abaixo:

cat valgrind-1.0.4.tar.bz2|bzip2 -dc|tar xf -
cd valgrind-1.0.4
./configure
make
su
make install

Isto instalará o Valgrind em /usr/local/bin. Você pode modificar passando a opção --prefix para o configure. Por exemplo, para que fosse instalado em /usr/bin:

./configure ~Wprefix=/usr

Depois de compilados os fontes, a utilização do Valgrind é surpreendentemente simples, não requerendo nenhuma alteração no código do programa a ser testado. Sua eficácia é maior, porém, se forem adicionadas informações de depuração ao programa. Veja o exemplo abaixo:

#include 

int main( int argc, char *argv[] )
{
	string *str = new string( ~SEste ponteiro não é liberado~T );
        return 0;
}

No exemplo, o ponteiro str é alocado e jamais liberado. Isto servirá de teste para o Valgrind. Para facilitar as coisas, compile o programa com informações de depuração e chame o Valgrind:

g++ -g3 -o leak leak.cpp
valgrind ~Wleak-check=yes ./leak

A execução do Valgrind com a opção --leak-check=yes faz com que o Valgrind faça a verificação para encontrar MLs. A saída do Valgrind é bastante longa, mas o trecho que nos interessa agora é:

==25516== 4 bytes in 1 blocks are definitely lost in loss record 1 of 2
==25516==    at 0x40163498: __builtin_new (vg_clientfuncs.c:126)
==25516==    by 0x80499A1: main (leak.cpp:5)
==25516==    by 0x402CDD7F: __libc_start_main (in /lib/libc-2.2.5.so)
==25516==    by 0x80498D1: __builtin_new (in /tmp/leak)

O que estas linhas dizem é que no processo 25516 (o programa leak) houve um vazamento de memória causado pela linha 5 do arquivo leak.cpp, justamente a linha onde o str foi alocado. Note que se não for usada a opção -g3 para o g++, o Valgrind não terá informações de número de linhas, portanto, é bastante recomendável que se compile o software a ser testado com esta opção.

Seguindo em frente, pode-se tentar causar outros erros que dão dor de cabeça na hora de serem encontrados. Veja:

#include 
#include 

int main( int argc, char *argv[] )
{
        string *str = new string( ~STeste~T );
        delete str;
        cout << str->c_str() << endl;
        return 0;
}

No exemplo acima, a memória apontada pelo ponteiro str é corretamente desalocada, mas depois disso, há uma tentativa de usar aquele endereço de memória novamente. Este bug é comum e muitas vezes causa efeitos completamente absurdos, muito difíceis de serem rastreados. Felizmente, o Valgrind vai apontar o problema:

==27072== Invalid read of size 4
...
==27072==    by 0x8049CF7: main (leak.cpp:8)
==27072==    Address 0x42BDF024 is 0 bytes inside a block of size 4 free'd
==27072==    at 0x401636FF: __builtin_delete (vg_clientfuncs.c:196)
...
==27072==    by 0x8049CD8: main (leak.cpp:7)
==27072==    by 0x402CDD7F: __libc_start_main (in /lib/libc-2.2.5.so)

Perceba que o Valgrind avisa que na linha 8 de leak.cpp está sendo feito um acesso a um espaço de memória liberado, e que esta liberação ocorreu na linha 7 do mesmo arquivo. Parece simples ao ver o programa de exemplo, mas estas informações são de grande valia em programas complexos em que a memória pode ter sido liberada em outra função de outro arquivo.

Seguindo em frente, o Valgrind pode também identificar o uso de ponteiros não inicializados:

#include 
#include 

int main( int argc, char *argv[] )
{
        string *str;
        cout << str->c_str() << endl;
        return 0;
}

No caso, declaramos um ponteiro chamado str para armazenar um objeto do tipo string. Mas antes de inicializarmos aquele espaço de memória, utilizamos uma função membro de string. Porém, na realidade, isto fará uma chamada para informações em um local da memória onde haverá apenas lixo. O Valgrind mais uma vez identifica o problema com detalhes:

==27187== Use of uninitialised value of size 4
...
==27187==    by 0x8048A4D: main (leak.cpp:7)

Em resumo, na linha 7 de leak.cpp estamos utilizando uma área não inicializada da memória.

No geral, o Valgrind é capaz de identificar as seguintes situações:

~U Uso de memória não inicializada;
~U Leitura e escrita de memória após ela ter sido liberada;
~U Leitura e escrita de dados fora de blocos malloc;
~U Leitura e escrita de áreas da pilha;
~U Vazamentos de memória;
~U Passagem de memória não endereçável ou não inicializada para chamadas de sistema;
~U malloc/new/new [] sem o free/delete/delete [] apropriado;
~U Maus usos da API de threads POSIX.

O Valgrind permite também que o gdb seja chamado aonde um erro for encontrado, de forma a permitir ao programador que o problema seja resolvido o mais rapidamente possível.

Muito projetos importantes vêm utilizando o Valgrind regularmente, e quanto mais projetos depurarem seu software com o Valgrind, melhor será a qualidade do software Open Source disponível em geral.

Otimizando o código

Outra função interessante do Valgrind é a de profiling de software. O profiling é utilizado para ajudar a otimizar um programa ao identificar as partes do sistema que são executadas mais vezes ou que demoram mais para serem executadas. Para este fim, existe um projeto chamado KCachegrind que fornece uma interface gráfica para análise dos resultados do Valgrind.

Saiba mais:
Valgrind - developer.kde.org/~sewardj
The design and imprementation of Valgrind - developer.kde.org/~sewardj/docs/techdocs.html


Roberto Teixeira - maragato@kde.org

A Revista do Linux é editada pela Conectiva S/A
Todos os Direitos Reservados.

Política de Privacidade
Anuncie na Revista do Linux